発生する問題
railsのroutingでresourceを定義しform_with
ヘルパーのmodel
引数にモデルのインスタンスを指定するとエラーになる。
再現手順
1. resourceで単一のリソースのCRUDを作成する
config/routes.rb
Rails.application.routes.draw do
resource :user
end
以下のpathが生成される
$ bundle exec rails routes | grep user
new_user GET /user/new(.:format) users#new
edit_user GET /user/edit(.:format) users#edit
user GET /user(.:format) users#show
PATCH /user(.:format) users#update
PUT /user(.:format) users#update
DELETE /user(.:format) users#destroy
POST /user(.:format) users#create
2. viewでform_withにmodelを指定する
app/views/users/new.html.erb
<%= form_with(model: user) do |form| %>
<% end %>
3. 画面でフォームをサブミットする
エラーになる
undefined method `users_path' for #<ActionView::Base:0x00000000009420>
Did you mean? user_path
原因
form_with
メソッドはmodel引数に渡されたインスタンスのクラスからurlのpathを判定する。
この判定ロジックで判定されたpathと、単一リソースのrouting(resource)定義で生成されるpathの間で不整合が発生するためエラーが発生する。
原因の説明
1. form_with
ヘルパーのurl
引数が存在しない場合、urlを判定するためにpolymorphic_path
を呼び出す
def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
options[:allow_method_names_outside_object] = true
options[:skip_default_ids] = !form_with_generates_ids
if model
url ||= polymorphic_path(model, format: format)
See: https://github.com/rails/rails/blob/main/actionview/lib/action_view/helpers/form_helper.rb#L737-L742
2. polymorphic_url
はHelperMethodBuilder.polymorphic_method
を呼び出す
def polymorphic_url(record_or_hash_or_array, options = {})
if Hash === record_or_hash_or_array
options = record_or_hash_or_array.merge(options)
record = options.delete :id
return polymorphic_url record, options
end
if mapping = polymorphic_mapping(record_or_hash_or_array)
return mapping.call(self, [record_or_hash_or_array, options], false)
end
opts = options.dup
action = opts.delete :action
type = opts.delete(:routing_type) || :url
HelperMethodBuilder.polymorphic_method self,
record_or_hash_or_array,
action,
type,
opts
end
3. HelperMethodBuilder.polymorphic_method
からbuilder.handle_model
が呼ばれる
def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
builder = get action, type
case record_or_hash_or_array
.
.
else
method, args = builder.handle_model record_or_hash_or_array
end
4. builder.handle_model
ではmodel.persisted?
がtrue
の場合はuser_path
、 false
の場合はusers_path
を返す
def handle_model(record)
args = []
model = record.to_model
named_route = if model.persisted?
args << model
get_method_for_string model.model_name.singular_route_key
else
get_method_for_class model
end
[named_route, args]
end
$ model #=> User
$ get_method_for_string model.model_name.singular_route_key #=> "user_path"
$ get_method_for_class model #=> "users_path"
5. createアクションなので対象のmodelはまだ存在しないため、users_path
がかえり、urlが存在しないエラーになっている
対応
1. routesでダイレクトルーティングを指定する
以下のようにインスタンスのクラスがUser
の場合はuser_path
に固定する。
config/routes.rb
Rails.application.routes.draw do
resource :user
+ resolve('User') { [:user] }
end
2. form_with
にurl引数を指定
app/views/users/new.html.erb
- <%= form_with(model: user) do |form| %>
+ <%= form_with(model: user, url: user_path) do |form| %>
<% end %>
参考
https://github.com/rails/rails/issues/1769
https://edgeguides.rubyonrails.org/routing.html#singular-resources
環境
rails 6.1.1