Railsアプリでお気に入り機能を実装したい！

# はじめに
現在railsでアプリを作成していてお気に入り機能を実装しました。
その備忘録としてアウトプットしたいと思います！

[完成イメージ](https://gyazo.com/582acdbccacbb57ffbda24d0a4a17915)

####前提

* Deviseでユーザー管理機能を実装済み
* ユーザーに紐づくモデルを作成済み（tweetモデルやarticleモデルなど）
* テンプレートエンジンはhamlを採用

# 開発環境
OS:　macOS Catalina
データベース： Mysql
バックエンド：　Ruby on Rails 6.0
テキストエディタ：　Visual Studio Code

# 目次
[1.データベース設計](#1-データベース設計)
[2.ルーティングの記述](#2-ルーティングの記述)
[3.アソシエーションの記述](#3-アソシエーションの記述)
[4.コントローラーの記述](#4-コントローラーの記述)
[5.Viewの記述](#5-Viewの記述)

## 1. データベース設計
さきにER図を御覧ください。
![スクリーンショット 2020-12-17 18.39.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/772095/d8c4dcbc-a88a-a802-6567-8b97a2c382b6.png)
今回は**Userテーブル**、**Eventテーブル**、**Favoriteテーブル**で考えたいと思います。
Eventテーブルとそのカラムについては任意の設定で大丈夫です。

FavoriteテーブルはUserテーブルとEventテーブルの**中間テーブル**となります。
そのため、カラムにはuser_idとevent_idを持たせます。
これで、どのユーザーがどのイベントをお気に入りしたかをデータベースに保存できます。

それでは、この設計に沿って準備をしていきます！

###下準備

```php:ターミナル
$rails g model Favorite user_id:integer tweet_id:integer
$rails g controller favorites create destroy
$rails db:migrate　#忘れずに
```
###マイグレーションファイルはこんな感じ！
![スクリーンショット 2020-12-17 18.52.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/772095/f6ca1ee7-1258-9cf0-89e1-5cc7e5511065.png)
次はルーティングの記述です

## 2. ルーティングの記述

```php:config/routes.rb
 #=====================================
  root 'events#index'
  devise_for :users

  resources :events do
    resource :favorites, only: [:create, :destroy]
  end
  #======================================
```
ポイントは2つです

* **favoritesアクションはeventアクションにネストさせる**
  こうすることで、お気に入りされたイベントの情報を引き出すのが楽になります

* **favoritesアクションはresorcesではなくresorceを用いる**
　favoriteはユーザーみたいに詳細ページや編集ページがいらないので個別のidはいりません。
　なので省略するために`resorce`を用います

## 3. アソシエーションの記述
favoriteはuserとeventに属する側なので`belongs_to`で記述します

```php:app/modelfavorite.rb
class Favorite < ApplicationRecord
    belongs_to :user
    belongs_to :event
end
```
userとeventモデルは以下のように記述します

```php:app/model/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :tweets
  has_many :favorites
  has_many :favorite_events, through: :favorites, source: :event 
end
```

```php:app/model/event.rb
class Event < ApplicationRecord
    belongs_to :user
    has_many :favorites
end
```

###User.rbに関して
####source
`source`は**「参照元のモデル」**をさすオプションです。
これを指定することでアソシエーションでメソッドチェーンする時の名称を変更することができます。

本当はhas_many :events, through: :favorites
と記述したいのですが、上のhas_many :eventsと重複してしまうため、favorite_eventsと名称を変更しています。

has many throughを使った「ユーザーがファボしたイベントの表示」
これで、`@user.favorite_events`とやることで、「ユーザーがファボしたイベント」を取得することができるようになります。



更に、お気に入り機能は登録だけでなく解除の機能も必要です。
そのため、Eventモデルに**「このイベントをユーザーがファボしているかどうか」**を判定するメソッドを用意します。

### ユーザーがイベントをお気に入りしたかどうかの判定メソッド

```php:app/model/event.rb
class Event < ApplicationRecord
    belongs_to :user
    has_many :favorites

# 追加
    def already_favorited?(user) #引数を受け取るように設定
        favorites.where(user_id: user.id).exists?
    end
end
```
普通メソッドはコントローラーの中に記述していくと思います。
僕自身、モデルにはアソシエーションやバリデーションだけを書く場所だと認識していましたがメソッドも定義することができます。

余談ですが、このようにモデルに定義するメソッドを**インスタンスメソッド**と呼びます。
インスタンスメソッドはクラスで生成されたインスタンスにメソッドを設定できます。

例えば、`events_controller.rb`が下記の場合を見てみましょう

```php:app/controllers/events_controller.rb
def show
  @event = Event.find(params[:id])
end
```
この場合の`@event`が**Eventクラスで生成されたインスタンスにあたります。**

この後のViewのセクションでも登場しますが`@event.already_favorited?(current_user)`と記述することで、現在ログインしているユーザーが対象のeventをお気に入りしているかどうかの判定をすることができるようになります！

## 4. コントローラーの記述

###UsersControllerの記述

```php:app/controllers/users_controllers.rb
class UsersController < ApplicationController
  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
    @events = @user.events
    @favorite_events = @user.favorite_events
  end
end
```
`@favorite_events = @user.favorite_events`と記述することで
Viewで`eachメソッド`を使用することで**「ユーザーがファボしたイベント」**を取得することができます。

###FavoritesControllerの記述

```php:app/controllers/favorites_controller.rb
class FavoritesController < ApplicationController
  def create
    favorite = current_user.favorites.build(event_id: params[:event_id])
    favorite.save
    redirect_back(fallback_location: root_path)
  end

  def destroy
    favorite = Favorite.find_by(event_id: params[:event_id], user_id: current_user.id)
    favorite.destroy
    redirect_back(fallback_location: root_path)
  end
end
```
###createアクション

アソシエーションを組んでいるので、`current_user.favorites`とすることでfavoriesテーブルのuser_idにcurrent_user.idを入れることができます。あとはevent_idを入れるのですが、、、

ポイントは`build`です。newでもcreateでも問題ないですが、インスタンスに紐づくインスタンスを生成するときにbuildが形式的に使われるのでこのように記述します。

createは自動的にsaveまで行いますが、buildはnewと同じく生成するだけなので`favorite.save`を記述してあげます。

####redirect_back
いいね、もしくはいいねを解除したときに同じページのままでいてほしいのでこのように記述します。
redirect_backで戻るべきページがなくてエラーがおきてしまう可能性があるので`(fallback_location: root_path)`と記述してエラーになるのを防ぎます。

###destroyアクション
特になしです笑
redirect_backに関してはcreateアクションと同じくいいねを解除した後も同じページに遷移するように記述しておきます。

## 5. Viewの記述
冒頭の通りHTMLではなくhamlで記述します。

###お気に入りボタンの実装

```php:app/views/events/show.html.haml
- if @event.already_favorited?(current_user)
  %p.favorite-heading
     保存をキャンセル
   = link_to event_favorites_path(@event), method: :delete, class: "favorite-link" do
     = icon("fas", "star", class: "favorite-icon")
- else
  %p.favorite-heading
     イベントを保存
   = link_to event_favorites_path(@event), method: :post, class: "favorite-link" do
     = icon("far", "star", class: "favorite-icon")
```

###解説

* **- if @event.already_favorited?(current_user)**

**user.rb**に定義した`already_favorited?(user)`メソッドをViewで使用します。
引数としてcurrent_userを渡すことで、現在ログインしているユーザーが対象イベントをファボしているかどうかの条件分岐をしています。

もし、すでにファボしている場合は`destroyアクション`をファボしていなければ`createアクション`を実行します。

* **= link_to~**

pathは`rails routes`で確認してタイプミスがないように気をつけましょう。
メソッドもしっかり記述しましょう。

地味ですが、最後の`do`は意外と躓いたポイントです。今回はアイコンを押してファボしたいので要素全体をリンクにするためにこのように記述します。

アイコンは**Font Awesome**でスターを使用します。
Font Awesomeの導入の仕方は[こちらをご覧ください](https://qiita.com/anago1030/items/655e215d8085d08cf6b6)


**以上で実装はおわりです！！**


# おわりに
最後まで読んでいただきありがとうございました！
改良点としては、いいねを非同期通信にできたらもうすこしUI/UXの点でよくなると思います。
お疲れさまでした。。

# 参考文献
[【初心者向け】丁寧すぎるRails『アソシエーション』チュートリアル【幾ら何でも】【完璧にわかる】🎸](https://qiita.com/kazukimatsumoto/items/14bdff681ec5ddac26d1#%E3%81%8A%E6%B0%97%E3%81%AB%E5%85%A5%E3%82%8A%E6%A9%9F%E8%83%BD%E3%82%92er%E5%9B%B3%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E8%A8%AD%E8%A8%88%E3%81%97%E3%82%88%E3%81%86)