Railsのビューで、edit_xxxx_path
といったURLヘルパーを使っているコードをレビューしていた際に気になったことがありました。
結論から言うと、
「idが要求されるパスをxxxx_pathで利用した時に、そのidを含むURLのページを開いていれば、idを引数に渡さなくても動作する」
という感じです。
例えば、現在開いているページが
localhost:3000/client/1
というページ(clients#show)で、ここから編集画面に遷移するリンクを作成するときは
edit_client_path
に引数を与えなくても自動的にeditアクションに渡すidを判断してくれる、ということです。
開発環境
- macOS Sierra 10.12.6
- Ruby 2.3.1
- Rails 4.2.4
気になったこと
以下はレビューしたコードの一部です。
ClientというリソースのCRUDをRESTfulなルーティングで定義しています。
...
<%= link_to "戻る", clients_path, class: 'btn btn-default' %>
<%= link_to "編集", edit_client_path, class: 'btn btn-success' %>
...
よくある詳細ページのリンク部分です。
このコードをレビューした僕は、edit_client_path
の引数にClientオブジェクト(@client)
またはClientのid(@client.id)
がないとエラーになるのではないかと考えました。
その旨をレビュイーに伝えると、「このコードでも動いた」と言うので、疑問に思った僕は自分のローカル開発環境で動かしてみました。動きました。
検証してみた
pryとpry-byebugを使って、以下の3パターンの動作を検証しました。
- Clientオブジェクトを引数に渡す
edit_client_path(@client)
- ClientオブジェクトのIDを引数に渡す
edit_client_path(@client.id)
- 引数なし
edit_client_path
Railsの内部のメソッドや変数といった情報を全て記載すると膨大な量になってしまうため、一部分のみ記載します。
show(詳細)ページをレンダリングしようとする際にedit_client_path
をRailsが解釈します。
Rails内部では、遷移元のコントローラー・アクションの情報と、遷移先のコントローラー・アクションの情報をハッシュで持ちます。
今回でいうと、遷移先の情報は
{:controller=>"clients", :action=>"edit"}
といったハッシュです。
そして遷移元の情報は、
{:host=>"localhost",
:port=>3000,
:protocol=>"http://",
:_recall=>{:controller=>"clients", :action=>"show", :id=>"1"},
:script_name=>""}
といった情報が入っています。
:_recall
というキーに対応する値が、直前のコントローラー・アクションの情報です。
この後、色々な加工がなされてブラッシュアップされていき、二つのハッシュが出来上がります。
{:controller=>"clients", :action=>"edit"}
と
{:controller=>"clients", :action=>"show", :id=>"1"}
です。
前者は遷移先のコントローラー・アクションの情報で、後者は遷移元のコントローラー・アクションの情報です。
最終的にこの二つのハッシュがマージされます。
マージされる場所はactiondispatch/journey/formatterのgenerate
メソッドです。
ここの
constraints = path_parameters.merge(options)
という行でマージされるのですが、変数の中身は
path_parameters #=> {:controller=>"clients", :action=>"show", :id=>"1"}
options #=> {:controller=>"clients", :action=>"edit"}
となっています。
mergeメソッドはこちらを見ていただければ分かりますが、二つのハッシュに重複するキーが存在する場合、値はmergeメソッドの引数に指定したハッシュの値が使われます。
そのため、constraints
変数には、
{:controller=>"clients", :action=>"edit", :id=>"1"}
というハッシュが格納されます。
このあと、Railsの内部でフォーマットされて"client/1/edit"
というパスが生成されますが、そのパスは上記のハッシュを元に作られます。
つまり、直前のアクションがidが要求されるアクションであれば、その情報を元にパスを生成する、と言えそうです。
なので、「直前のアクションで要求されたidのオブジェクトとは別のオブジェクトに対して処理をする」ということになると、その処理を施したいオブジェクトのidを渡してあげる必要がありますね。
普通に引数を渡す場合
edit_client_path
に引数を渡す場合、
edit_client_path(@client)
-
edit_client_path(@client.id)
といった渡し方があると思います。
二つのハッシュをmergeメソッドでマージする段階のハッシュの中身を見てみると、遷移元の情報は今までと同じですが、遷移先の情報は、
{:controller=>"clients",
:action=>"edit",
:id=>
#<Client:0x007f8547852748
id: 1,
corporation_id: 1,
name: "テストクライアント2",
tel: "12345678901",
postal_code: "111-1111",
address: "テスト住所",
person_in_charge: "テスト担当者",
email: "test@test.client",
note: "テスト2",
created_at: Tue, 12 Sep 2017 21:20:44 JST +09:00,
updated_at: Tue, 12 Sep 2017 21:40:01 JST +09:00,
deleted_at: nil>}
{:controller=>"clients",
:action=>"edit",
:id=>"1"}
というハッシュになっています。
これらをmergeメソッドの引数に指定してマージするので、:id
というキーの値は遷移先の情報で上書きされます。
1に関してはidではなくオブジェクトの情報ですが、この後の処理によってidだけ上手く取り出されるようになっているようです。
まとめ
editなどのidを必要とするリンクは、状況によってはidなしで作成してもエラーにならないという話でした。
ただ使いどころは限定されているかなと感じます。
-
showアクションで詳細ページを表示 -> editアクションへのリンク
が今回の例でしたが、 -
editアクションで「詳細ページに戻る」ボタンを作る
などの限定的な使い方しかできなさそうですね。なにかいい使い方の例があったらぜひ教えてください。
以上です。