Railsで作ったSNSアプリでユーザの投稿へのコメント作成機能を実装する

・参考URL
https://sadah.github.io/rails-training/ja/004_comments.html

今回紹介するコードは、プログラミングスクールで学んだTwitterクローンを基にしたRailsのアプリ(Micropost)への追加機能であることを前提にするとコードが読みやすくなると思います。

#モデルの作成
```
rails g model Comment content:string user:references micropost:references
```
コメントはUserとMicropostの多：多の関係性を表すため、中間テーブルが必要になります。
Userがコメントする投稿が複数あって、MicropostにはコメントしたUserが複数いるためです。
###マイグレーションファイル
```ruby:年月日時_create_relationships.rb
class CreateComments < ActiveRecord::Migration[5.2]
  def change
    create_table :comments do |t|
      t.string :content
      t.references :user, foreign_key: true
      t.references :micropost, foreign_key: true

      t.timestamps
    end
  end
end
```
マイグレーションを忘れずに。

```
rails db:migrate
```
自動作成されたコメントモデルにバリデーションを付けておきましょう。

###コメントモデル
```ruby:comment.rb
class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :micropost

  validates :content, presence: true, length: { maximum: 255 }
end
```
コメントを文字なしで投稿できないように設定します。

#関連モデル

###Userモデル
```ruby:user.rb
class User < ApplicationRecord
  has_many :microposts
  has_many :comments
end
```
一つのUserに対して多数のコメントがあるためこの関係になります。
###Micropostモデル

```ruby:micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
end
```
一つのMicropostに対して多数のコメントがあるためこの関係になります。

Micropostモデルに`dependent: :destroy`を追記したことで、投稿が削除されるとそれに紐づいたコメントが削除されるようになります。
`dependent: :destroy`がないと、コメントの付いた投稿を削除するときにエラーが起きます。

```ruby:micropost.rb
has_many :commenters, through: :comments, source: :user
```
Micropost側から見ると、コメントしてくるユーザが多数いるので、正確にモデル作成するなら以下のコードの追記が必要かもしれません。(今回使うことはありませんでしたが、間違っていたらごめんなさい)
#ルーティング
```ruby:routes.rb
Rails.application.routes.draw do
  resources :microposts, only: [:create, :destroy, :show] do
    resources :comments, only: [:create, :destroy]
  end
end
```
どのMicropostに対してコメントなのかを明らかにするためにルーティングのネスト(入れ子構造)をします。
#コントローラー作成

```
$ rails g controller comments create destroy
```
コメントで必要な機能は作成機能(のちに削除機能)の2つだけなのでターミナルへの入力はこのようになります。

```ruby:comments_controller.rb
class CommentsController < ApplicationController
  before_action :require_user_logged_in
  
  def create
    @micropost = Micropost.find(params[:micropost_id]) 
    @comment = @micropost.comments.build(comment_params)
    @comment.user_id = current_user.id
    if @comment.save
      flash[:success] = '投稿にコメントしました。'
      redirect_back(fallback_location: root_path)
    else
      @micropost = Micropost.find(params[:micropost_id]) 
　　　 @comments = @micropost.comments.includes(:user)
      flash.now[:danger] = '投稿へのコメントに失敗しました。'
      render 'microposts/show'
    end
  end

  private
  
  def comment_params
    params.require(:comment).permit(:content)
  end
end
```
コメントはmicropostと紐づいているため`@micropost`を頭につけます。
コメントとuserを紐づけるために`@comment.user_id = current_user.id`が必要です。
これがないとコメントからuserデータを得られません。

else文が実行されるときrenderするファイルに`@micropost`と`@comments`の
2つのインスタンス変数を渡すのは、micropostのshowファイルを表示するために必要だからです。
片方でも欠けるとエラーになります。

###関連コントローラー
コメント作成を投稿の詳細ページ(show)で実行できるようにします。

```ruby:microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :require_user_logged_in

  def show
    @micropost = Micropost.find(params[:id])
    @comments = @micropost.comments.includes(:user)
    @comment = @micropost.comments.build(user_id: current_user.id) if current_user # form_with 用
  end
end
```

showページの`@micropost = Micropost.find(params[:id])`で特定の一つのmicropostを表示できるように設定します。

`@comments = @micropost.comments.includes(:user)`により、対応する投稿へのコメントを一覧表示することができます。
`includes(:user)`はコメントしたuserを表示するためにuserカラムを取得するメソッドです。これがないと`gravatar_url`などを表示する時に`user`がnilになりエラーが起こります。

`@comment`には、コメント入力フォームの表示エラーを避けるために空のコメントを入れておきます。

```ruby:comments_controller.rb
class CommentsController < ApplicationController
  before_action :require_user_logged_in
  
  def create
    @micropost = Micropost.find(params[:micropost_id]) 
    @comment = @micropost.comments.build(comment_params)
    @comment.user_id = current_user.id
    if @comment.save
      flash[:success] = '投稿にコメントしました。'
      redirect_back(fallback_location: root_path)
    else
      @micropost = Micropost.find(params[:micropost_id]) 
      @comments = @micropost.comments.includes(:user)
      flash.now[:danger] = '投稿へのコメントに失敗しました。'
      render 'microposts/show'
    end
  end
  
  private
  
  def comment_params
    params.require(:comment).permit(:content)
  end
end
```
基本的には`microposts_controller`のcreateと同じようにします。(microposts_controllerに関してはRails Tutorialなどを参考にしてください)
コメントは`micropost`に紐づくように作成したいので、対応する`micropost`の
インスタンスを記入しておきます。
`@comment.user_id = current_user.id`がないとコメントにuser情報が入らないため必須です。
`@micropost`と`@comennts`両方のインスタンスを代入しないとrender先のshowファイルで
コメント失敗メッセージを表示できずにエラーが起こります。

#投稿詳細ページのルーティングの確認
投稿詳細ページに飛ぶためのルーティングをターミナルを使って確認します。

```
$ rails routes
```
`microposts#show`ページに飛ぶために必要なPrefixを確認します

```
micropost GET    /microposts/:id(.:format)      microposts#show
```
投稿一覧のパーシャルに以下の１行を追加して、投稿詳細ページに移動できるようにしましょう。

```erb:microposts一覧のパーシャルファイルの一部
<% microposts.each do |micropost| %>
〜
<%= link_to 'Comments', micropost_path(micropost), class: 'btn btn-link btn-sm' %>
〜
<% end %>
```
microposts#showのPrefixが`micropost`なので`micropost_path`と記入します。
micropost一覧から特定の`|micropost|`のページに飛びたいので、
`micropost_path`の()内には||内の`micropost`を代入します。

#Viewファイル

###投稿詳細ページ
コメントしたい特定のmicropostを表示するファイルです。
micropostを表示させるためには、controllerの`def show`で定義した
`@micropost`のインスタンスをshowファイルに渡せるように注意しましょう。

```erb:microposts/show.html.erb
<ul class="list-unstyled">
  <li class="media mb-3">
    <img class="mr-2 rounded" src="<%= gravatar_url(@micropost.user, { size: 50 }) %>" alt="">
    <div class="media-body">
      <div>
        <%= link_to @micropost.user.name, user_path(@micropost.user) %> <span class="text-muted">posted at <%= @micropost.created_at %></span>
      </div>
      <div>
        <p><%= @micropost.content %></p>
      </div>
      <div class="btn-group">
        <% if current_user == @micropost.user %>
          <%#=詳細ページで削除するとid見つからないエラー発生 link_to "Delete", @micropost, method: :delete, data: { confirm: "You sure?" }, class: 'btn btn-danger btn-sm' %>
        <% end %>
      <%= render 'favorites/favorite_button', micropost: @micropost %>
      </div>
    </div>
  </li>
</ul>

<%# コメント入力フォームのパーシャル %>
<%= render 'comments/form', micropost: @micropost %>

<%# コメント一覧のパーシャル %>
<%= render 'comments/comments', micropost: @micropost %>
```
投稿詳細ページでmicropostを削除すると、micropostの削除自体はできても
ルーティングエラーが起きてしまうため、現在は削除ボタンを使えないようにしています。

###コメント入力フォーム
`form_with`のmodelに`@micropost`と`@comment`が入るようにしないとエラーが起こります。

```erb:comments/_form.html.erb
<div class="col-sm-8">
<%= form_with(model: [@micropost, @comment], local: true) do |f| %>
  <div class="form-group">
    <%= f.text_area :content, class: 'form-control', rows: 3 %>
  </div>
  <%= f.submit 'Comment', class: 'btn btn-primary btn-block' %>
<% end %>
</div>
```
###コメント一覧
```erb:comments/_comments.html.erb
<ul class="list-unstyled">
  <% @comments.each do |comment| %>
    <li class="media mb-3">
      <img class="mr-2 rounded" src="<%= gravatar_url(comment.user, { size: 50 }) %>" alt="">
      <div class="media-body">
        <div>
          <%= link_to comment.user.name, user_path(comment.user) %> <span class="text-muted">posted at <%= comment.created_at %></span>
        </div>
        <div>
          <p><%= comment.content %></p>
        </div>
        <div class="btn-group">
          <% if current_user == comment.user %>
            <%#= link_to "Delete", micropost_comment_path(comment), method: :delete, data: { confirm: "You sure?" }, class: 'btn btn-danger btn-sm' %>
          <% end %>
        </div>
      </div>
    </li>
  <% end %>
  <%#= paginate comments %>
</ul>
```
コメント削除機能は正しいルーティングができていないので、まだ未実装です。