【Rails】いいね機能の実装

## 始めに
忘れるといけないので、deviseと合わせたいいね機能の実装を簡単にまとめました。

## 前提 
環境
　Rails 5.2系
　Ruby 2.6系

使用ライブラリ
　devise
　Slim

## 実装

### 1.Railsアプリの作成

```
$ cd
$ rails new favorite_function
$ cd favorite_function
```

---

### 2.gem deviseとSlim導入

```ruby:Gemfile

gem 'slim-rails'  #Slimのジェネレータを提供
gem 'html2slim'  #ERB形式のファイルをslim形式に変換してくれる
gem 'devise'
```

```
$ bundle
```

---

### 3.deviseをインストール

```
$ rails g devise:install
    create  config/initializers/devise.rb
    create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

===============================================================================
```

---

### 4.deviseインストールと同時に指示される4つの指示に従い編集を行う
####　4-1.　1つ目

```ruby:config/environments/development.rb

Rails.application.configure do
.
.
.
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
.
.
.
end
```

####　4-2.　2つ目

```
$ rails g controller posts new index show
```

```ruby:config/routes.rb

Rails.application.routes.draw do
  root to: 'posts#index'  #自分がアプリに設定しようと思っているroot_urlを指定
  resources :posts
  .
  .
  .
end
```

####　4-3.　3つ目

```ruby:app/views/layouts/application.html.erb

<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
```

####　4-4.　4つ目

```
$ rails g devise:views

invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/devise/shared
      create    app/views/devise/shared/_error_messages.html.erb
      create    app/views/devise/shared/_links.html.erb
      create    app/views/devise/confirmations
      create    app/views/devise/confirmations/new.html.erb
      create    app/views/devise/passwords
      create    app/views/devise/passwords/edit.html.erb
      create    app/views/devise/passwords/new.html.erb
      create    app/views/devise/registrations
      create    app/views/devise/registrations/edit.html.erb
      create    app/views/devise/registrations/new.html.erb
      create    app/views/devise/sessions
      create    app/views/devise/sessions/new.html.erb
      create    app/views/devise/unlocks
      create    app/views/devise/unlocks/new.html.erb
      create    app/views/devise/mailer
      create    app/views/devise/mailer/confirmation_instructions.html.erb
      create    app/views/devise/mailer/email_changed.html.erb
      create    app/views/devise/mailer/password_change.html.erb
      create    app/views/devise/mailer/reset_password_instructions.html.erb
      create    app/views/devise/mailer/unlock_instructions.html.erb
```

---

### 5.ここで全てのビューをSlimに変換する

```
$ bundle exec erb2slim app/views/ --delete
```

　__※deviseのディレクトリに関してはSlimに変換するとdevise/shared/_error_messages.html.slimでエラーが起きるので修正する。__

```ruby:devise/shared/_error_messages.html.slim
- if resource.errors.any?
  #error_explanation
    h2

      #以下の3行でエラーが出るので以下のように修正
      = I18n.t("errors.messages.not_saved",
        count: resource.errors.count,
        resource: resource.class.model_name.human.downcase)

    ul
      - resource.errors.full_messages.each do |message|
        li
          = message
```

---

### 6.認証機構のビューを作成

```ruby:app/views/layouts/application.html.slim

html
  head
  .
  .
  .
  body
    - if user_signed_in?
      =link_to 'ログアウト', destroy_user_session_path, method: :delete
    - else
      =link_to '新規登録', new_user_registration_path
      =link_to 'ログイン', new_user_session_path
  .
  .
  .
```
　・deviseの、どのパスがどのアクションへ繋がっているかわからなくなったらrails routesコマンドで確認

---

### 7.モデルの作成

```
$ rails g devise User
$ rails g model Post content:string user:references 
$ rails g model Favorite user:references post:references
$ rails g migration add_columns_to_users username:string

$ rails db:migrate
```

　・```モデル名 : references```とすることで、自動でマイグレーションファイル内でforeign_keyを張ってくれ、モデルファイル内で関連付けも行ってくれる。

　・```UserとPostは1対多```の関係なので、Postに対してUserの所有であることを示すreferenceを張る。

　・```UserはPostに対する複数のいいねを持ち```、```PostもUserからの複数のいいねを持つ``````多対多```の関係なので、Favoriteに対してPostとUserの所有であることを示すreferenceを張る。

　・```deviseで作成されるデフォルトのUserモデルにはカラム追加できない```ので、カラム追加コマンドを実行しnameなどのUserに関するデータを作成。

---

### 8.モデルの関連付け

####　8-1.　Favoriteモデル

```ruby:app/models/favorite.rb
class Favorite < ApplicationRecord
  belongs_to :user
  belongs_to :post
end
```

####　8-2.　Postモデル

```ruby:app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :favorites
end
```

　・投稿に付いたいいねを取得する際にpost.favoritesという呼び出しが可能になる。post.favorites.countとすればいいね数を取得できる。


####　8-3.　Userモデル

```ruby:user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :favorites
  has_many :favorite_posts, through: :favorites, source: :post
  has_many :posts
end
```

　　・```ユーザーの付けたいいねを取得```する際にuser.favoritesという呼び出しが可能になる。

　　・```ユーザーがいいねを付けた投稿を自分のプロフィール画面に表示させたい場合```がある。その際にuser.favorite_postsという呼び出しができるようになる。これは```throughオプション```を使い、中間テーブルを通してPostモデルへと関連付けている。

　　・throughで使うモデルは、```必ず先に関連付けを行っていなければならない```。この場合だとfavoritesを先に関連付けていなければならない。

　　・sourceには、命名したモデル名の参照元となるモデル名を記述する。
```has_many :好きなモデル名, through: :中間テーブル, source: :命名したモデルの参照元となるモデル名```

---

### 9.コントローラの作成

```
$ rails g controller posts new index show  #deviseの設定の際に作成してる場合は省略
$ rails g controller users index show
$ rails g controller favorites create destroy
```

　・(posts、)users、 favoritesコントローラ

---
### 10.ユーザー系機能の実装

####　10-1.　ルーティングの編集

```ruby:config/routes.rb

Rails.application.routes.draw do

  resources :users, only: [:index, :show]  # ユーザー一覧やプロフィール画面用
  devise_for :users  # ログイン機構用

end
```

####　10-2.　usersコントローラの編集

```ruby:app/controllers/users_controller.rb

class UsersController < ApplicationController

  before_action :authenticate_user!  # deviseが用意してくれてるuser認証

  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
  end
end
```

####　10-3.　usersのビューの編集

#####　　●　indexビュー

```ruby:app/views/users/index.html.slim
h1 ユーザー一覧

- @users.each do |user|
  p
    = link_to user.email, user
```

#####　　●　show

```ruby:app/views/users/show.html.slim
h1 ユーザープロフィールページ

p
  = @user.username
  = @user.email
```

####　10-4.　applicationビューにユーザー系ページへのリンクを張る

```ruby:app/views/layouts/application.html.slim
  body
    - if user_signed_in?
      .
      .
      .
   +  = link_to 'マイページ', user_path(current_user)
   +  = link_to 'ユーザー一覧', users_path
    - else
      .
      .
```

---
### 11.投稿系機能の実装

####　11-1.　ルーティングの編集

```ruby:config/routes.rb
Rails.application.routes.draw do

  resources :users
  devise_for :users

  root to: 'posts#index'  # 投稿一覧をルートに指定
  resources :posts

end
```

####　11-2.　postsコントローラの編集

```ruby:app/controllers/posts_controller.rb
class PostsController < ApplicationController

  def index
    @posts = Post.all
  end

  def new
    @post = Post.new
  end

  def create
    @post = current_user.posts.new(post_params)  # current_userはdeviseが用意してくれる、ログイン最中のユーザーを表す
    if @post.save
      redirect_to @post
    end
  end

  def show
    @post = Post.find(params[:id])
  end

  private
  def post_params
    params.require(:post).permit(:content)
  end
end
```

　　　・　新規投稿される際、作成した投稿の所有者を投稿したユーザーにしたいので```current_user.posts```とする。これで、postsテーブルのuser_idにはcurrent_userのidが格納される。（```UserモデルとPostモデルの関連付けを行っていないと、この方法は使えないので注意```）

####　11-3.　postsのビューの編集

#####　　●　index

```ruby:app/views/posts/index.html.slim
h1 投稿一覧

- @posts.each do |post|
  p= link_to post.user.email, user_path(post.user.id)
  p= link_to post.content
```
　　
#####　　●　new

```ruby:app/views/posts/new.html.slim
h2 新規投稿

= form_with model: @post do |f|
  p
    = f.label :content
    = f.text_area :content
  = f.submit '投稿'
```
　　　
#####　　●　show

```ruby:app/views/posts/show.html.slim
h1 投稿詳細ページ

p= @post.content
```

####　11-4.　applicationビューに投稿系ページへのリンクを張る

```ruby:app/views/layouts/application.html.slim
  body
    - if user_signed_in?
      .
      .
      .
      = link_to '投稿一覧', posts_path
    - else
      .
      .
```

---
### 12.いいね機能の実装

####　12-1.　投稿にいいねボタンを作成

```ruby:app/views/posts/index.html.slim
h1 投稿一覧

- @posts.each do |post|
  p= link_to post.user.email, user_path(post.user.id)
  p= link_to post.content

  - if user_signed_in?  # deviseが用意してくれてる、ユーザーがログインしてるかどうかを確かめるメソッド
    - if post.favorited_by?(current_user)
      p= link_to post.favorites.count, post_favorites_path(post.id), method: :delete
    - else
      p= link_to post.favorites.count, post_favorites_path(post.id), method: :post
  - else
    = post.favorites.count
```

　　　・```ログインしているユーザーだけが見れるようにする```ために、```user_signed_in?```を用いる。

　　　・```createアクションにていいねが付き```、```destroyアクションにていいねが消える```仕組みなので、link_toヘルパーのmethodにそれぞれ明示的に指定してあげる。

　　　・```いいねをした場合はいいねの削除リンクを表示```させ、```いいねをまだしていない場合はいいねの作成リンクを表示させたい```ので、```favorited_by(current_user)メソッド```でそれを確認する。

　　　・favorited_by(current_user)メソッドはこの後作成する。

　　　・```post.favorites.countでは、その投稿のいいね数を表示させている```。

####　12-2.　いいねの有無を確認するメソッドを定義

```ruby:app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :favorites

  
  + def favorited_by?(current_user)
  +   favorites.where(user_id: current_user.id).exists?
  + end
end
```
　　　・Postモデルに対してだけ呼び出すメソッドのため、Postのインスタンスメソッドとして定義する。

　　　・```favoritesテーブルのuser_idにログイン中のユーザーのidが存在するか```を確かめている。

　　　・post.favorited_by(current_user)みたいな感じで使うことになる。少し簡単にして見てみる

```ruby:posts/index.html.slim
h1 投稿一覧

- @posts.each do |post|
  .
  .
  .
  - if post.favorited_by?(current_user) 
      # いいねの削除リンクを表示
  - else
      # いいねの作成リンクを表示
```

　　　・これは以下のコードを示している。

```ruby
- if post.favorites.where(user_id: current_user.id).exists?
    # 削除リンク表示
- else
    # いいねの作成リンクを表示
```

　　　・```取得したpostのfavoritesテーブルから```、```user_idがログイン中のユーザーのidであるものを取得```させて、```それが存在するかどうかを確認```させている。

　　　・存在すればtrueを返す。```いいねしてあるという意味```なので、いいね削除リンクを表示させる。

　　　・存在しなければfalseを返す。```いいねしてないという意味```なので、いいね作成リンクを表示させる。

####　12-3.　ルーティングの編集

```ruby:config/routes.rb
Rails.application.routes.draw do

  resources :users
  devise_for :users

  root to: 'posts#index'
  resources :posts do
+   resource :favorites, only: [:create, :destroy]
  end

end
```

　　　・favoritesはposts内にネストさせる。これにより```post_favorites_path```などとして、投稿に対していいねを付けるためのパスの指定が簡単になる。　

####　12-4.　favoritesコントローラの編集

```ruby:app/controllers/favorites_controller.rb
class FavoritesController < ApplicationController

  def create
    favorite = current_user.favorites.build(post_id: params[:post_id])
    favorite.save
    redirect_to posts_path
  end

  def destroy
    favorite = Favorite.find_by(post_id: params[:post_id], user_id: current_user.id)
    favorite.destroy
    redirect_to posts_path
  end
end
```
　　　・```ログイン中のユーザーのfavoritesテーブルに```、```いいねを押した投稿のidを格納する```。そして保存することでいいねの付与と見なさせている。

　　　・```いいね解除を押した投稿のid```と、```ログイン中のユーザーのidが一致```するfavoritesテーブルのデータを取得して、消す。そうすることでいいね解除と見なさせている。

####　12-5.　ユーザーのページで、そのユーザーがいいねした投稿を表示する。ついでに、そのユーザーが投稿した投稿たちも表示させる。
#####　　●　usersコントローラのshowアクションを編集

```ruby:app/controllers/users_controller.rb
class UsersController < ApplicationController

  before_action :authenticate_user!

  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
    @posts = @user.posts
    @favorite_posts = @user.favorite_posts
  end
end
```

　　　　・`@posts`では、普通の投稿を表示するために通常の投稿を取得してる。

　　　　・`@favorite_posts`では、ユーザーがいいねした投稿を取得してる。app/models/user.rbで、favoritesテーブルを通してfavorite_postsという名前でPostモデルと関連付けを行わせたのはこのため。

#####　　●　show.html.slimを編集する

```ruby:app/views/users/show.html.slim
h1 ユーザープロフィールページ

p
  = @user.username

h3 いいね
- @favorite_posts.each do |f_post|
  p
    = f_post.content

h3 投稿
- @posts.each do |post|
  p
    = post.content
```
　　　　・上はユーザーがいいねした投稿を表示する。

　　　　・下はユーザーが投稿した投稿たちを表示する。

　　　　・クリックイベントによる切り替えを行うとTwitterみたいな感じにできますが、ここではやりません。


以上で、いいね機能の実装は完了です。


## 大いに参考とした記事
　https://qiita.com/kazukimatsumoto/items/14bdff681ec5ddac26d1