娯楽開発

娯楽開発

開発は娯楽。遊び心で開発をすすめるブログ。

【謎挙動】form_tagとtext_fieldで検索フォームを作成【rails】

検索フォームを作ろうとしてform_tagtext_fieldを使った際に謎の挙動が見られたのでメモ書き。(一応解決されました。)

環境は以下の通りです。

結論

まず結論。 form_tagtext_fieldを使って検索フォームを実装する場合、以下のように書くのが正解なようです。

# tasks_controller.rb

def index
  @tasks = Task.all
end

def search
  search_word = "content like '%" + params[:task][:content] + "%'"
  @tasks = Task.where(search_word)
end
<!-- index.html.erb -->

<body>
  <div>
    <%= form_tag search_path do %>
      <%= text_field :task, :content %>
      <%= submit_tag "検索" %>
    <% end %>
  </div>
...
...
</body>

これで検索フォームに入力した単語(search_word)を持つデータがTaskテーブルからsearchアクションの@tasksに入力されます。

問題の謎挙動

さて早速ですが問題の挙動です。

以下のようなコードを書いた時に起こりました。

# tasks_controller.rb

def index
  @tasks = Task.all
  @task = Task.new
end

def search
  search_word = "content like '%" + params[:content] + "%'"
  @tasks = Task.where(search_word)
end

まずコントローラーを上のように設定し、indexページにsearchアクションを仕込ませてparams[:content]を検索ワード(search_word)として抜き出そうという狙いでした。(Taskテーブルにcontentカラムを持たせています。)

そして実際にindexページに書いた間違いコードが以下の二つ。

<!-- index.html.erb 間違いその1 -->

<div>
  <%= form_tag search_path do %>
    <%= text_field @task, :content %>
    <%= submit_tag "検索" %>
  <% end %>
</div>
<!-- index.html.erb 間違いその2 -->

<body>
  <%= form_tag search_path @task do |f| %>
    <%= text_field f, :content %>
    <%= submit_tag "検索" %>
  <% end %>
</body>

これらはどちらもform_tagtext_fieldの用法としては間違いです。

しかしその2ではparams[:content]でうまく検索ワードを抜き出せてしまいました。

なぜ「その2」では間違った記法にも関わらず正しい挙動をしたのか、また「その1」と「その2」はほとんど等価なプログラムのように見えるのになぜ「その1」では正しく挙動しないのか、わかったことを簡単にまとめます。

paramsを見てみる

まず、これらの書き方によって得られるparamsを見てみます。

searchアクションにbinding.pryを仕込んでparamsの中身を調べます。

# index.html.erb 間違いその1

[1] pry(#<TasksController>)> params
=> <ActionController::Parameters {"utf8"=>"", "authenticity_token"=>"wB4+spI/twQq+OS7wGOkjP7rYuGlDN/kXYbeN9lESqFTHSU38EcWMmG00Z5cVGKl0EFcQ5HSf2qpijb8r+zCuA==", "#<Task:0x00007f9f327081d0>"=>{"content"=>"hoge"}, "commit"=>"検索", "controller"=>"tasks", "action"=>"search"} permitted: false>
# index.html.erb 間違いその2

[1] pry(#<TasksController>)> params
=> <ActionController::Parameters {"utf8"=>"", "authenticity_token"=>"IE7LzRafXk1idX7E5tHpQMsvs1WsRrnv1Udbcbm2aRCzTdBIdOf/eyk5S+F65i9p5YWN95iYGWEhS7O6zx7hCQ==", "content"=>"hoge", "commit"=>"検索", "controller"=>"tasks", "action"=>"search"} permitted: false>

その1ではparamsの中に"#<Task:0x00007f9f327081d0>"=>{"content"=>"hoge"}というハッシュがネストしており、params[:content]では"hoge"にアクセスできない状態になっています。

一方その2ではparamsハッシュの中にうまく"content"=>"hoge"が格納されており、params[:content]"hoge"にアクセスできていますね。

間違いその1について

<!-- index.html.erb 間違いその1 -->

<div>
  <%= form_tag search_path do %>
    <%= text_field @task, :content %>
    <%= submit_tag "検索" %>
  <% end %>
</div>

その1で間違っているのはtext_fieldの使い方です。

<%= text_field hoge, fuga %>
<%= submit_tag %>

とした場合、text_field文字列 "hoge"をキーとしたハッシュ"hoge"=>{"fuga"=>"入力文字"}paramsに返します。

この辺りがtext_fieldのよしなに力を感じる部分で、仮にhoge:hogeでも@hogeでも@@hogeであったとしてもいったん文字列"hoge"としてキーにするようです。

今回の間違いその1ではhogeに該当する部分に@taskが入っていました。 この@taskはコントローラーのindexアクションから空のクラスTask.newを受け取ってきます。 Task.newを文字列に変換すると

>> Task.new.to_s
=> #<Task:0x00007f9f327081d0>

となるので、結果としてparamsには"#<Task:0x00007f9f327081d0>"=>{"content"=>"hoge"}というハッシュが出来上がります。

ちなみにtext_fieldにはもうちょっとだけrailsのよしなに力が働いていて、キーとして与えられた文字列(例でいうhogefuga)が変数として使われていないかindexアクションまで確認に行ってくれているようです。

そのため今回の場合だと@tasksは複数のクラスを抱えていることを認知しているため

<%= text_field @tasks, fuga %>

のような使い方をすると、@tasksをキーとするには不適切と判断しエラーを出力します。

hogefugaは基本的にはどんな文字列でも大丈夫ですが、そのページで使用するクラス変数などは中身を参照してよしなに判断されてしまうので注意が必要です。

さすがはrailsさん、完全に初見殺し気が利いていますね。

間違いその2について

<!-- index.html.erb 間違いその2 -->

<body>
  <%= form_tag search_path @task do |f| %>
    <%= text_field f, :content %>
    <%= submit_tag "検索" %>
  <% end %>
</body>

続いてその2でparams[:content]にアクセスできている理由です。

そもそも論なんですが、form_tagではその2のようなブロック変数|f|は受け取らない設計になっています。

ですので「その2」のdo-endブロック内においてブロック変数fnilとして扱われています。

よって<%= text_field f, :content %>fは無視され paramsに直接"content"=>"hoge"が入力されることになります。

本来の用法ではありませんがエラーを吐かずに動いてしまうため、製作者の意図に反してこのような挙動をとってしまう可能性があります。 こちらも注意が必要です。

まとめ

text_field

  • 引数をいったん文字列としてparamsのキーにする
  • 使用できる文字列かをよしなに判断してくれる
    • 配列やハッシュなど複数の値を持つ場合はエラーを吐く可能性がある

form_tag

  • ブロック変数は取らない
  • ブロック変数を持たせても動作するが、ブロック変数はnil扱いとなる

思う存分にrailsマジックに翻弄された事例でした。 皆さんも十分にお気をつけください。