紅孔雀 このページをアンテナに追加 RSSフィード

2012-02-26

[] Rails 4.0 では HTTP の PATCH メソッドで更新する


In practice, as you see, PATCH suits everyday web programming way better than PUT for updating resources. In Ruby on Rails it corresponds naturally to the way we use update_attributes for updating records.

Thus, PATCH is going to be the primary method for updates in Rails 4.0.

Edge Rails: PATCH is the new primary HTTP method for updates

Rails のブログ記事を読みましたが、Rails 4.0 では更新処理に HTTP の PATCH メソッドを利用するそうです。正確には、今まで使用されていた PUT メソッドと PATCH メソッドのどちらの場合もコントローラの update メソッドが呼び出されます。

HTTP の PATCH メソッドについては「そんなメソッドもあったかな」程度しか認識がありませんでしたので、上記の記事や検索して調べました。PUT と PATCH の違いは次の通りです。

  • PUT - クライアントが送信した内容がサーバ上でのリソースの新しい内容となる。
  • PATCH - サーバ上に既に存在するリソースの一部を更新する。

例えば、ブログ記事の変更フォームや会員サイトのパスワード変更フォームなどを考えると、そこではリソースの一部しか更新しません。つまり、PUT メソッドではなく PATCH メソッドが適切ということになります。

Rails では PUT や DELETE メソッドなどに対応していない Web サーバに対応するために、hidden フィールド()で本来のメソッドを送信しているので、その部分で patch という値が送信されるようになります。アプリケーションコードでは、コントローラで request.patch? で判定することになります。

2012-01-15

[]Thinプロセスを順番に再起動する方法

「thin --onebyone restart」のように「--onebyone」オプションを付けておくと、プロセスを一つずつ再起動するようになることを知ったのでメモしておきます。

Rails アプリケーションを動作させるときに Thin を使うことがあります。再起動するときは「thin -C webapp.yml restart」などとしますが、このときに全プロセスが終了後、新しいプロセスが起動されます。一時的に全プロセスが停止した状態となるため、どうしても瞬断するタイミングが出来てしまい不便に感じていました。

瞬断を発生させずに再起動するために、設定ファイルを二つに分け、「thin -C webapp1.yml restart」「thin -C webapp2.yml restart」という具合に半分ずつ再起動するようにしていました。

thin のヘルプを見ていて気づいたのですが、「--onebyone」というオプションを指定すると、プロセスが一つずつ再起動されることを知りました。つまり、以下のようなどうさになります。

プロセスAを終了
    ↓
プロセスAを起動
    ↓
プロセスBを終了
    ↓
プロセスBを起動
    :

「--onebyone」オプションを指定すれば、瞬断を避けるために設定ファイルを二つに分ける必要はなくなります。

2011-12-24

[] Fixture 間の関連を ID ではなく名前で設定する

O'Reilly のサイトで Rails 3 in a Nutshell という本の内容が公開されていました。その中の 11. Testing の「Defining relationships between fixtures」で解説されていたのですが、Fixture 間の関連は ID ではなく名前で設定することができるそうです。

例として、ユーザ(User)が記事(Article)を投稿する機能があるとします。

ユーザテーブル (users) のカラムには「名前(name)」と「メールアドレス(email)」があり、モデルの定義は以下のようになります。

# app/models/user.rb
class User < ActiveRecord::Base
  validates :name, :presence => true
  validates :email, :presence => true
end

記事テーブル(articles)のカラムには「投稿ユーザ ID(user_id)」と「タイトル(title)」、「本文(body)」があり、モデルではユーザへの関連(belongs_to :user)を記述しておきます。

# app/models/article.rb
class Article < ActiveRecord::Base
  belongs_to :user
  validates :title, :presence => true
  validates :body, :presence => true
end

ユーザ(User)の Fixture では「taro」及び「jiro」を定義します。

# test/fixtures/users.yml
taro:
  name: たろう
  email: taro@example.com

jiro:
  name: じろう
  email: jiro@example.com

記事(Article)の Fixture では「taro」及び「jiro」の記事データを登録します。その際、上で定義した「taro」や「jiro」を使用して関連を設定することができます。

# test/fixtures/articles.yml
taro_greeting:
  user: taro            # users.yml で定義した名前で指定する。
  title: 自己紹介
  body: たろうです。

jiro_greeting:
  user: jiro            # users.yml で定義した名前で指定する。
  title: 自己紹介
  body: じろうです。

2011-12-19

[] テーブルに存在しない属性は型変換が行われない

Rails でデータベースにカラムが存在しない属性を扱いたい場合、通常は attr_accessor で属性を定義すると思います。しかし、attr_accessor で定義した属性は、テーブルにカラムが存在する属性のように、代入時の型変換が行われません。

例えば、以下のような会員クラスに利用規約の同意フラグを保持する属性を attr_accessor :accepted で定義したとします。

# 会員.
class User < ActiveRecord::Base

  # 利用規約の同意フラグ.
  attr_accessor :accepted

end

会員登録フォームの View ファイルでは、利用規約に同意する場合はチェックボックスにチェックするものとします。

<%= form_for(@user) do |form| %>
  ...
  利用規約に同意しますか?
  <%= form.check_box(:accepted) %>はい
  ...
<% end %>

利用規約に同意されなかった場合はエラーとしたいのですが、会員登録を行うコントローラで以下のように記述しても正しく動作しません(常に同意したものとして処理されてしまいます)。

def create
  @user = User.new(params[:user])
  unless @user.accepted
    # 利用規約に同意していないのでエラー
  end
end

原因は、accepted は attr_accessor で定義されているためです。利用規約のチェックボックスがチェックされなかった場合にクエストパラメータとして '0' が送られてくるのですが、accepted には文字列としての '0' がそのまま保持されます。Ruby では '0' も真として扱われるため、チェックしたかしないかによらず unless @user.accepted の内部には入りません。本当は true/false に型変換されると嬉しいのですが。

[] テーブルに存在しない属性でも型変換を行う方法

テーブルに存在しない属性でも型変換を行う方法を考えます。

Rails では、カラムの情報は ActiveRecord::ConnectionAdapters::Column クラスが保持しています。そして、代入時の型変換は ActiveRecord::ConnectionAdapters::Column#type_cast メソッドで行われます。

# ActiveRecord::ConnectionAdapters::Column.new(name, default, sql_type = nil, null = true)
column = ActiveRecord::ConnectionAdapters::Column.new(:accepted, false, :boolean)
column.type_cast('1') # => true
column.type_cast('0') # => false

そこで、attr_accessor の変わりに、代入時に ActiveRecord::ConnectionAdapters::Column#type_cast で型変換を行うメソッドを定義します。

class ActiveRecord::Base
  def self.column(name, type, default=nil)
    ActiveRecord::ConnectionAdapters::Column.new(name, default, type).tap do |column|
      define_method("#{name}=") do |value|
        instance_variable_set("@#{name}", column.type_cast(value))
      end
      define_method(name) do
        unless instance_variable_defined?("@#{name}")
          instance_variable_set("@#{name}", column.default)
        end
        instance_variable_get("@#{name}")
      end
    end
  end
end

そして、上記の column メソッドでカラムがテーブルに存在しない属性を定義します。

# 会員.
class User < ActiveRecord::Base

  # 利用規約の同意フラグ.
  column :accepted, :boolan, false

end

これで代入時に型変換が行われるようになりました。

user = User.new(:accepted => '1')
user.accepted # => true

user = User.new(:accepted => '0')
user.accepted # => false

2011-12-18

[][]ハッシュで初期化可能な構造体

Ruby には Struct という構造体を作成するクラスがあります。

例えばユーザ情報(名前とメールアドレス)を保持する構造体を作成する場合は以下のように記述します。

User = Struct.new(:name, :email)

user = User.new('taro', 'taro@example.com')
user.name  # => 'taro'
user.email # => 'taro@example.com'

ところで Rails を使用していると、ハッシュをキーワード引数のように使用するシーンが多々あります。例えばモデルクラスを初期化するときや、メソッドの引数などです。これに慣れてくると、構造体も以下のようにインスタンス化できれば便利だと感じます。

# このように書きたい.
user = User.new(:name => 'taro', :email => 'taro@example.com')

そこで、ハッシュで初期化可能な構造体を考えます。

まずはハッシュで初期化するための initialize メソッドを提供するモジュールを作成します。

module HashInitializable
  def initialize(attributes={})
    attributes.each do |name, value|
      send("#{name}=", value)
    end
  end
end

この HashInitializable を Struct.new で作成した構造体クラスに include すれば、ハッシュで初期化可能な構造体が出来上がります。

User = Struct.new(:name, :email)
User.send(:include, HashInitializable)

user = User.new(:name => 'taro', :email => 'taro@example.com')
user.name  # => 'taro'
user.email # => 'taro@example.com'

HashInitializable は単なるモジュールなので、Struct 以外にも使用可能です。

上記の User を通常のクラスとして以下のように定義することもできます。

class User
  attr_accessor :name, :email
  include HashInitializable
end

user = User.new(:name => 'taro', :email => 'taro@example.com')
user.name  # => 'taro'
user.email # => 'taro@example.com'

それでは最後に、HashInitializable を使用して、ハッシュで初期化可能な構造体を作成します。

class HashInitializableStruct
  def senf.new(*arguments)
    struct = Struct.new(*arguments)
    struct.send(:include, HashInitializable)
    struct
  end
end

これで以下のように記述することができるようになりました。

User = HashInitializableStruct.new(:name, :email)

user = User.new(:name => 'taro', :email => 'taro@example.com')
user.name  # => 'taro'
user.email # => 'taro@example.com'

2011-12-11

[] rake タスクを再定義する方法

rake タスクを定義するときに、既に同じ名前のタスクが存在する場合は、処理内容が「再定義(上書き)」されるのではなく、定義した順に処理が実行されます。

例えば、以下のように hello というタスクを 2 回定義しているとします。

# lib/tasks/hello.rake

desc 'Hello1'
task :hello do
  puts 'Hello1'
end

desc 'Hello2'
task :hello do
  puts 'Hello2'
end

「rake hello」で hello タスクを実行すると、タスクを定義した順に実行されることが分かります。

$ rake hello
Hello1
Hello2

「rake -T hello」で hello タスクの情報を表示すると、定義した内容がそれぞれ実行されることが分かります(desc で指定した説明が '/' で区切られて表示されます)。

$ rake -T hello
rake hello  # Hello1 / Hello2

定義済みの内容に独自の処理を追加したい場合は、単純に同じ名前のタスクを定義すれば良いです。

反対に、定義済みの処理内容を再定義(上書き)したい場合は、以下のように定義済みのタスク情報をクリアする必要があります。

# lib/tasks/hello.rake

desc 'Hello1'
task :hello do
  puts 'Hello1'
end

# 定義済みタスクの情報をクリアする.
task = Rake.application.lookup('hello')
task.clear
task.instance_variable_set('@full_comment', nil)

desc 'Hello2'
task :hello do
  puts 'Hello2'
end

上記のように、Rake.application.lookup で指定した名前のタスクを取得することができます。

そして、task.clear でタスクの処理をクリアします。また、インスタンス変数の @full_comment を nil にリセットすることで、「rake -T」で出力されるタスクの説明をクリアすることができます。

「rake hello」で hello タスクを実行すると、以下のように最後に定義した処理だけが実行されることが分かります。

$ rake hello
Hello2

「rake -T hello」でタスクの情報を表示しても、最後に定義したものだけが出力されることを確認できます。

$ rake -T hello
rake hello  # Hello2

2011-12-08

[] テーブルごとに初期データを登録する

Rails でテーブルごとに初期データを登録できるように改良する方法を考えます。


Rails では db/seeds.rb に初期データを登録するスクリプトを記述します。

db/seeds.rb は rake の db:seed タスクにより実行されます。

rake db:seed

この仕組みを改良し、テーブル単位で初期データを登録できるようにします。

アプローチは色々考えられますが、ここでは以下のように改良する方法を考えます。

  • db/seeds.rb をテーブルごとに分割して作成する。
  • 分割したファイルを個別に実行する rake タスクを定義する。

db/seeds.rb をテーブルごとに分割する

まずは db/seeds.rb をテーブルごとに分割します。

分割したファイルは db/seeds というディレクトリを作成し、その中に保存することにします。

具体的には、以下のようになります。

db/seeds/shops.rb
db/seeds/products.rb
db/seeds/prefecture.rb
  :

rake タスクを定義する

次に、db/seeds/*.rb を個別に実行する rake タスクを定義します。

独自の rake タスクを定義するには、lib/tasks ディレクトリに .rake という拡張子のファイルを作成します。

ここでは以下の内容で lib/tasks/seed.rake ファイルを作成します。

# db/seeds/*.rb にマッチするファイルごとにタスクを定義する.
Dir.glob(File.join(Rails.root, 'db', 'seeds', '*.rb')).each do |file|
  desc "Load the seed data from db/seeds/#{File.basename(file)}."
  task "db:seed:#{File.basename(file).gsub(/\..+$/, '')}" => :environment do
    load(file)
  end
end

以下のコマンドで定義したタスクが有効になっていることを確認します。

rake -T db:seed

結果は以下のようになります。

rake db:seed             # Load the seed data from db/seeds.rb.
rake db:seed:shops       # Load the seed data from db/seeds/shops.rb.
rake db:seed:products    # Load the seed data from db/seeds/products.rb.
rake db:seed:prefectures # Load the seed data from db/seeds/prefectures.rb.
  :

db/seeds.rb を修正する

最後に db/seeds.rb に db/seeds/*.rb を読み込むコードを記述します。

Dir.glob(File.join(Rails.root, 'db', 'seeds', '*.rb')) do |file|
  load(file)
end

これで「rake db:seed」とすれば全ての初期データが登録され、「rake db:seed:shops」などとすれば個別のテーブルごとに初期データを登録することができるようになりました。

2011-10-02 Rails3 でのメール送信

[] Rails3 のメール送信で本文を 8bit で送信する

というわけで困ってたんですが、さっきspecを見たりしながら調べたら、どうもMail::Message#transport_encodingを8bitに設定すればよいようです。

Rails3で送信するメールの本文をbase64ではなく8bitにする方法 - 思っているよりもずっとずっと人生は短い。

Rails3 で何も設定を行わずにメール送信を行うと、本文が base64 エンコーディングされたのですが、以下のように transport_encoding = '8bit' を指定すれば UTF-8 のまま送信されるようです。

mail = Mailer.notice('お知らせです')
mail.transport_encoding = '8bit'
mail.deliver

メールを送信する全ての箇所を上記のように書き換えるのは手間なので、次のように mail メソッドを置き換えて、デフォルトで transport_encoding = '8bit' を設定するようにしました。*1

# app/mailers/mailer.rb
class Mailer < ActionMailer::Base

  def notice(body)
    @body = body
    mail(:to => 'xxx@example.com')
  end

  protected

  def mail_with_default_settings(headers={}, &block)
    mail_without_default_settings(headers, &block).tap do |mail|
      mail.transport_encoding = '8bit'
    end
  end
  alias_method_chain :mail, :default_settings

end

*1:PC 向けのメール送信と携帯向けのメール送信で設定を変えたい、という場合もあるはずなので、ActionMailer::Base#mail を直接変更するのではなく、ActionMailer::Base の派生クラスごとに対応することにしました。

2010-08-01

[] Model クラスの一覧を取得する方法

Model クラスの一覧を取得するには、以下のアプローチが考えられます。

  1. テーブル名からクラス名を求める方法
  2. 全クラスから ActiveRecord::Base を継承するクラスを抽出する方法

テーブル名からクラス名を求める方法

方法
tables = ActiveRecord::Base.connection.tables
=> ["schema_migrations", "users", "photos"]

models = tables.map{|table| Object.const_get(table.classify) rescue nil}.compact
=> User(id: integer, email: string, created_at: datetime), Photo(id: integer, us
er_id: integer, file_name: string, created_at: datetime)]
問題

以下のように table_name でテーブル名を設定している(クラス名がテーブル名の単数形になっていない)クラスを取得することが出来ません。

class Image
  self.table_name = 'photos'
end
対策

クラス名とテーブル名を Rails の規約に沿うように決めることで、上記の問題は発生しなくなります。

全クラスから ActiveRecord::Base を継承するクラスを抽出する方法

方法
constants = Object.constants.map{|name| Object.const_get(name)}
=> ["2.3.5", ApplicationController, OpenSSL, Rational, Signal, "i686-linux", Pre
ttyPrint, TSort, MonitorMixin, StringScanner::Error, Module, NKF, Socket, Except
... (省略)

constants.select{|c| c.class == Class and c < ActiveRecord::Base and !c.abstract_class?}
=> [Photo(id: integer, user_id: integer, file_name: string, created_at: datetime
), User(id: integer, email: string, created_at: datetime)]
問題

ロードされていないクラスを取得することができません。例えば、development モードで実行しているとき *1 はリクエストごとに Model クラスがキャッシュされないため、そのリクエストでアクセスされなかった Model クラスを取得することが出来ません。

対策

$RAILS_ROOT/app/models/ 以下の *.rb ファイルを事前に require すれば回避できます。

*1:正確には config.cache_classes = false が設定されているとき

2009-11-03 ページごとに <title> の値を変更する方法

[] ページごとに <title> の値を変更する方法

Rails でページごとに <title> の値を変更する方法についてまとめます。

通常、ヘッダやフッタなどの共通部分は app/views/layouts に置かれるレイアウトファイルに記述し、ボディ部分を個々の View ファイルに記述します。

このとき問題となるのが、レイアウトファイルに共通部分として記述されている内容の一部を変更するにはどうするか、ということです。例えば、「ページごとに <title> で指定するページのタイトルをどのように変えたい」という場合です。

[] ページごとに <title> の値を変更する方法(content_for を使用する)

content_for を使用する

ページごとに <title> の値を変更するには content_for メソッドを使用することができます。

例として以下の構成を考えます。

  • app/views/layout/default.html.erb(レイアウト用のファイル)
  • app/views/users/index.html(ユーザ一覧ページ)
  • ...(他のページ)

この場合、app/views/layout/default.html.erb(レイアウト用のファイル)の内容は以下のようになります。



<html>
<head>

<title><%= yield :title %></title>
</head>
<body>
<%= yield %>
</body>

そして、app/views/users/index.html(ユーザ一覧ページ)の内容は以下のようになります。




<% content_for :title do %>
ユーザ一覧
<% end %>


...

結果として、以下のような HTML が生成されます。

<html>
<head>
<title>ユーザ一覧</title>
</head>
<body>
...
</body>

同様に、他のページにも「content_for :title」を追加すれば、ページごとに <title> の値を変更することができます。

content_for を使用する方法の問題点

content_for を使用する方法でやりたいことは実現できるのですが、以下の点に不満が残ります。

  • ページのタイトル文字列が個々の View ファイルに分散してしまう。
  • 個々の View ファイルで「content_for :title」を忘れた場合に気付きにくい(「yield :title」の部分が空白になるだけ)。

[] ページごとに <title> の値を変更する方法(page_title メソッドを実装する)

やりたいこと

やりたいことをまとめると以下の通りです。

  • ページのタイトル文字列は一箇所で管理したい。
  • 個々の View ファイルに特別な処理は書きたくない。

方針

やりたいことを実現するために、以下の方針を採ることにします。

  1. 各ページのタイトルは config/locales/ja.yml に記述する*1
  2. 現在表示しているページを判別し、それに対応する値を config/locales/ja.yml から取得する。

具体的には config/locales/ja.yml には以下のように記述します。

# config/locales/ja.yml

ja:
  # このセクションに各ページのタイトル文字列を記述することにします
  title:
    users:
      index:  ユーザ一覧
      show:   ユーザ詳細
      new:    ユーザ登録
      create: ユーザ登録
      edit:   ユーザ編集
      update: ユーザ編集
  ...

そして、app/views/layout/default.html.erb(レイアウト用のファイル)は以下のように記述できる仕組みを作ります。



<html>
<head>

<title><%= page_title %></title>
</head>
<body>
<%= yield %>
</body>

page_title メソッドを実装する

現在表示しているページのタイトル文字列を返す page_title メソッドを ApplicationHelper に実装します。

config/locales/ja.yml に記述したタイトル文字列の取得は、以下のように I18n.t メソッドで行うことが出来ます。

I18n.t('title.users.index') # => 'ユーザ一覧'

ですので、これから作成する page_title メソッドでは「I18n.t('title.XXX.YYY')」の「XXX」と「YYY」の部分に指定する値(コントローラ名とアクション名)を準備すれば良いことになります。

現在アクセスされているページのコントローラ名とアクション名はそれぞれ「controller_name」および「action_name」で取得することができる*2ので、page_title メソッドの実装は以下のようになります。

# app/helpers/application_helper.rb

module ApplicationHelper

  # 現在のページのタイトルを取得する
  def page_title
    t("title.#{controller_name}.#{action_name}")
  end

end

page_title メソッドの問題点

ここで実装した page_title メソッドにはいくつかの問題点(制限)があります。それは、config/locales/ja.yml の以下の部分です。

# config/locales/ja.yml

ja:
  title:
    users:
      index:  ユーザ一覧
      show:   ユーザ詳細
      new:    ユーザ登録
      create: ユーザ登録 # new と同じ値を指定する必要がある
      edit:   ユーザ編集
      update: ユーザ編集 # edit と同じ値を指定する必要がある
  ...

コメントに書いた通り、「# new と同じ値を指定する必要がある」と「# edit と同じ値を指定する必要がある」というのが制限になります。

理由は、render メソッドで描画するテンプレートを明示的に指定された場合に対応できていないためです。以下のコードを見てください。

class UsersController < ActionController::Base

  def new
    @user = User.new
  end

  def create
    @user = User.new(params[:user])

    if @user.save
      redirect_to :action => index
    else
      render :action => :new # 登録画面が表示されるが action_name の値はあくまで 'create'
    end
  end

  ...

end

コメントに書いたとおり、「render :action => :new」としても「action_name」の値はあくまで 'create' となるため、先ほど作成した page_title メソッドでは「ja: > :title > :users > :create」の値が返されます。

そのため、config/locales/ja.yml の「new と create」および「edit と update」に異なる値を指定すると、validation エラーが発生したときだけページのタイトル文字列が変わってしまうという問題が発生します。

page_title メソッドの問題点 (2)

今度は「登録画面」の次に「確認画面」を入れる場合を考えます。

config/locales/ja.yml の内容は以下のようになります。

# config/locales/ja.yml

ja:
  title:
    users:
      index:        ユーザ一覧
      show:         ユーザ詳細
      new:          ユーザ登録
      confirm_new:  ユーザ登録確認 # この場合はどうしよう?
      create:       ユーザ登録
      edit:         ユーザ編集
      confirm_edit: ユーザ編集確認 # この場合はどうしよう?
      update:       ユーザ編集
  ...

すると、「new と confirm_new に同じ値を指定する」というアプローチでは苦しくなります。登録確認画面のタイトルは「ユーザ登録」ではなく「ユーザ登録確認」としたいのですが、そうしてしまうと登録画面で validation エラーが発生した場合に、ページのタイトルが「ユーザ登録確認」となってしまいます。

[] ページごとに <title> の値を変更する方法(page_title メソッドを改良する)

方針

render メソッドでテンプレート名が明示的に指定された場合に問題があることが分かったので、以下の方針で対応します。

  1. render メソッドをオーバーライドしてレンダリング対象のテンプレート名を @current_render_target に保持する。
  2. page_title メソッドでは @current_render_target の値からページのタイトル文字列を取得する。

render メソッドをオーバーライドする

render メソッドは ApplicationController でオーバーライドします。

render メソッドが呼び出されるパターンと @current_render_target に設定する値をまとめます。

  • 引数がハッシュの場合
    • (A) 「render :action => :new」のようにテンプレート名が指定される場合は指定されたテンプレート名を設定する。
    • (B) テンプレート名が指定されない場合は action_name の値を設定する。
  • 引数がハッシュではない場合
    • (C) 「render :new」のようにテンプレート名が指定される場合は指定されたテンプレート名を設定する。
    • (D) テンプレート名が指定されない場合(明示的に render メソッドが呼ばれない場合)は action_name の値を設定する。

これを実装すると以下のようになります。

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base

  def render(options = nil, extra_options = {}, &block)

    # レンダリング対象のテンプレート名を取得する
    if render_options.kind_of?(Hash)
      # (A) または (B) の場合
      @current_render_target = render_options[:action] || action_name
    else
      # (C) または (D) の場合
      @current_render_target = render_options || action_name
    end

    # レンダリング処理自体は ActionController::Base.render に任せる
    super
  end

  ...

end

page_title メソッドを改良する

ApplicationHelper に作成した page_title メソッドは以下のようになります。

# app/helpers/application_helper.rb

module ApplicationHelper

  # 現在のページのタイトルを取得する
  def page_title
    # 変更前
    # t("title.#{controller_name}.#{action_name}")

    # 変更後
    t("title.#{controller_name}.#{@current_render_target}")
  end

end

これで render メソッドでテンプレート名が指定された場合にも正しく動作するようになりました。

page_title メソッドをもう少し改良する

page_title メソッドをもう少し改良して、

  • 引数で指定されたページのタイトルを返す
  • 引数が省略された場合は現在のページのタイトルを返す

とすれば、より便利になります。

実装は以下のようになります。

# app/helpers/application_helper.rb

module ApplicationHelper

  # 現在のページのタイトルを取得する
  def page_title(controller=controller_name, action=@current_render_target)
    t("title.#{controller}.#{action}")
  end

end

[] ページごとに <title> の値を変更する方法(まとめ)

長くなりましたので、最終的に行ったことをまとめます。

メッセージリソース(config/locales/ja.yml)

メッセージリソース(config/locales/ja.yml)には各ページのタイトル文字列を記述します。

# config/locales/ja.yml

ja:
  # 各ページのタイトル文字列
  title:
    users:
      index:  ユーザ一覧
      show:   ユーザ詳細
      new:    ユーザ登録
      create: ユーザ登録
      edit:   ユーザ編集
      update: ユーザ編集
  ...

レイアウト用のファイル(app/views/layout/default.html.erb)

レイアウト用のファイル(app/views/layout/default.html.erb)では page_title メソッドで取得した値を <title> に指定します。



<html>
<head>

<title><%= page_title %></title>
</head>
<body>
<%= yield %>
</body>

ApplicationController(app/controllers/application_controller.rb)

ApplicationController では render メソッドをオーバーライドしてレンダリング対象のテンプレート名を @current_render_target に保持します。

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base

  def render(options = nil, extra_options = {}, &block)

    # レンダリング対象のテンプレート名を取得する
    if render_options.kind_of?(Hash)
      @current_render_target = render_options[:action] || action_name
    else
      @current_render_target = render_options || action_name
    end

    # レンダリング処理自体は ActionController::Base.render に任せる
    super
  end

  ...

end

ApplicationHelper(app/helpers/application_helper.rb)

ApplicationHelper では @current_render_target に保持されているレンダリング対象のテンプレート(または引数で指定されたページ)のタイトル文字列を取得します。

# app/helpers/application_helper.rb

module ApplicationHelper

  # 現在のページのタイトルを取得する
  def page_title(controller=controller_name, action=@current_render_target)
    t("title.#{controller}.#{action}")
  end

end

*1:config/locales/ja.yml に記述することで簡単に国際化に対応することができます。

*2:「controller_name」、「action_name」はどちらも ActionController::Base で定義されています。