Railsチュートリアル - Webフォームのコードを共通化する際に、フォームのPOST先をどうyieldするか

# 記事内容

[Railsチュートリアル 第10章 10.1.1 編集フォームの演習2](https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-exercises_edit_form)で手こずった部分と、その解決策に関する備忘録です。

# 実装の全体像

実装の全体像としては、以下のようになります。

* バージョン 5.1.6 のRails
* `users#new`と`user#edit`に、`form`というパーシャルを埋め込む
    * ソースの実体は埋め込みRuby(erb)
    * 埋め込みそのものは、erbの`render`メソッドで行う

# うまくいかない実装

## 実装内容

以下のような実装だとうまくいきません。

まずは`_form.html.erb`。`form`パーシャルの実体です。

```erb:app/views/users/_form.html.erb
<%= form_for(@user, yield(:path)) do |f| %>
  ...略
<% end %>
```

続いて`new.html.erb`。`new`ビューの実体です。

```erb:app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<% provide(:button_text, 'Create my account') %>
<% provide(:path, signup_path) %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
  </div>
</div>
```

最後は`edit.html.erb`。`edit`ビューの実体です。

```erb:app/views/users/edit.html.erb
<% provide(:title, 'Edit user') %>
<% provide(:button_text, 'Save changes') %>
<% provide(:path, user_path) %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
    ...略
  </div>
</div>
```

## 実行結果

HTTPリクエストが、500 Internal Server Errorで失敗します。

```
Started GET "/users/2/edit" ...略
Processing by UsersController#edit as HTML
  Parameters: {"id"=>"2"}
  User Load (5.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Rendering users/edit.html.erb within layouts/application
  Rendered users/_form.html.erb (9.4ms)
  Rendered users/edit.html.erb within layouts/application (31.0ms)
Completed 500 Internal Server Error in 115ms (ActiveRecord: 5.2ms)


  
ActionView::Template::Error (no implicit conversion of Symbol into Integer):
    1: <%= form_for(@user, yield(:url)) do |f| %>
    2:   <%= render 'shared/error_messages', object: @user %>
    3: 
    4:   <%= f.label :name %>
  
app/views/users/_form.html.erb:1:in `_app_views_users__form_html_erb___3024067975909551957_69984483370220'
app/views/users/edit.html.erb:8:in `_app_views_users_edit_html_erb__939350102988132727_69984483174380'
```

エラーメッセージには、「no implicit conversion of Symbol into Integer」とあります。

# そもそも`no implicit conversion of Symbol into Integer`というエラーが出る理由と、その解決

## `no implicit conversion of Symbol into Integer`というエラーが出る理由

Railsの`form_for`メソッドの最終引数は、オプションハッシュとなります。一方、今回`form_for`メソッドの最終引数として与えた`yield(:url)`というのはハッシュではありません。それが原因となってエラーが発生するに至るわけです。

## オプションハッシュとは

Railsで広く使われるテクニックとして、「オプションハッシュ」というものがあります。「特定のメソッドにおいて、必須引数以外に0個以上の可変個の引数を取りたい」という場合に使われるテクニックです。「最後の引数がハッシュである場合、`{}`は省略できる」というRubyの言語仕様をうまく活用した例といえるでしょう。

ハッシュであるため、その値は「キーと値の組」でなければなりません。

## 解決方法

この場合、`form_for`の第2引数に与える正しいキーは`:url`です。というわけで、`_form.html.erb`内で以下の修正が必要となります。

```diff:
- <%= form_for(@user, yield(:path)) do |f| %>
+ <%= form_for(@user, url: yield(:path)) do |f| %>
```

# うまくいく実装

## 実装内容

まずは`_form.html.erb`。`form`パーシャルの実体です。

```erb:app/views/users/_form.html.erb
<%= form_for(@user, url: yield(:path)) do |f| %>
  ...略
<% end %>
```

続いて`new.html.erb`。`new`ビューの実体です。

```erb:app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<% provide(:button_text, 'Create my account') %>
<% provide(:path, signup_path) %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
  </div>
</div>
```

最後は`edit.html.erb`。`edit`ビューの実体です。

```erb:app/views/users/edit.html.erb
<% provide(:title, 'Edit user') %>
<% provide(:button_text, 'Save changes') %>
<% provide(:path, user_path) %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
    ...略
  </div>
</div>
```

## 実行結果

HTTPリクエストは、200 OK で正常終了します。

```
Started GET "/users/2/edit" ...略
Processing by UsersController#edit as HTML
  Parameters: {"id"=>"2"}
  User Load (8.5ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Rendering users/edit.html.erb within layouts/application
  Rendered shared/_error_messages.html.erb (0.5ms)
  Rendered users/_form.html.erb (27.1ms)
  Rendered users/edit.html.erb within layouts/application (55.7ms)
  Rendered layouts/_rails_default.erb (224.7ms)
  Rendered layouts/_shim.html.erb (0.4ms)
  User Load (3.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Rendered layouts/_header.html.erb (8.2ms)
  Rendered layouts/_footer.html.erb (0.5ms)
Completed 200 OK in 507ms (Views: 464.5ms | ActiveRecord: 11.7ms)
```

# なぜ`edit_user_path`ではなく`user_path`か

ルーティングの定義における以下の記述がポイントになります。

```ruby:config/routes.rb
Rails.application.routes.draw do
  # ...略
  resources :users
end
```

`resources :users`という形でルーティングが定義されていますね。この場合、Railsの仕組み上、以下の定義が丸ごと与えられます。

* `/users`に対して`GET`リクエストを投げると、`users#index`アクションが実行される
* `/users/new`に対して`GET`リクエストを投げると、`users#new`アクションが実行される
* `/users`に対して`POST`リクエストを投げると、`users#create`アクションが実行される
* `/users/:id`に対して`GET`リクエストを投げると、`users#show`アクションが実行される
* `/users/:id/edit`に対して`GET`リクエストを投げると、`users#edit`アクションが実行される
* `/users/:id`に対して`PATCH`リクエストまたは`PUT`リクエストを投げると、`users#update`アクションが実行される
* `/users/:id`に対して`DELETE`リクエストを投げると、`users#destroy`アクションが実行される

この場合、`/users/:id`の内容を更新してほしければ、単に`user_path(:id)`に対して`PUT`リクエストを投げればよいのです。

[2.2 CRUD、動詞、アクション - Rails のルーティング - Rails ガイド](https://railsguides.jp/routing.html#crud%E3%80%81%E5%8B%95%E8%A9%9E%E3%80%81%E3%82%A2%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3)も読んでみるとよいです。

# まとめ

* `form_for`の第2引数はオプションハッシュであり、キーと値の組が必要となる
* `form_for`のオプションハッシュ`:url`には、値としてRailsの_pathヘルパーをそのまま与えることもできる
* 埋め込みRubyの`provide`と`yield`では、Railsの_pathヘルパーをそのままやり取りすることができる

# 関連リンク

* [Ruby - 【Railsチュートリアル】パーシャル実装によるルーティングの不正｜teratail](https://teratail.com/questions/66002)