Rails5でカート機能を作るためのロジックを作ってみた


# 今回書くこと
- 以前からショッピングカート機能を作りたい作りたいと思っていて、ようやく作れたのでそのもっとも簡単なルートを書いておく。
- ビューをどのように書くかは今回は趣旨ではないため割愛するが、どのページにどのような操作を置くといいかは書く
- 別にこれはベストのロジックだと断定はしません。ただ、いろいろ調べてみて、一番しっくりきたのを使っています

# 参考にしたページ
- 最初[こちらのページ](https://richonrails.com/articles/building-a-shopping-cart-in-ruby-on-rails)を真似て作ってみたのですが、Railsのバージョンの問題（？）もあって綺麗に動かなかったので、動くように作り直したものになります

# 作る手順
## モデル作り
- 今回はログイン機能をつけないので、Userモデルは割愛します。
- Userモデルを使う場合はCartモデルをhas_manyさせてあげればいいように使えるかと

```bash
rails g model product name price:integer
rails g model cart
# cart_itemのquantityに対しては、必ずdefaultで0を入れて置くこと
rails g model cart_item quantity:integer product:references cart:references
```

- `product`は商品モデルで、言わずもがな商品名と価格が入る。お好みで原価とかを入れてもいいかも
- `cart`はカートモデル。商品カートを表し、 **セッションで管理する**(どのページでも必要になるので、いちいちPOSTするよりも、セッションにIDを持たせておいた方が楽だから)
- `cart_item`はカートにはいっている商品モデル。productとcartの中間テーブル的な役割を持ち、 **カートに入っている特定の商品の個数を管理するのに必須**。

```ruby
class Product < ApplicationRecord
end

class Cart < ApplicationRecord
  has_many :cart_items
end

class CartItem < ApplicationRecord
  belongs_to :product
  belongs_to :cart
end
```

## helperメソッド、`current_cart`の定義
- ショッピングカート作りの肝となるメソッドの作成
- これを定義することで、セッション情報を元に、現在のカートを呼び出すことができるようになる

```ruby
# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  helper_method :current_cart

  def current_cart
    if session[:cart_id]
      @cart = Cart.find(session[:cart_id])
    else
      @cart = Cart.create
      session[:cart_id] = @cart.id
    end
  end
end
```

## 必要なルーティング設定
- 今回使うルーティングは以下のようになる

```ruby
Rails.application.routes.draw do
  root 'home#index'

  resources :products
  
  resources :carts, only: [:show]

  post '/add_item' => 'carts#add_item'
  post '/update_item' => 'carts#update_item'
  delete '/delete_item' => 'carts#delete_item'
end
```

- ルートディレクトリは別にいきなり`products#index`にしてしまってもいいが、ここは私の好みにしてある。
- 今回作る流れとしては、

```
1. 商品一覧画面から一つの商品を選び、個数を入力して「購入」ボタンを押す
2. カート詳細画面に移動し、現在カートに入っている商品一覧を出す。
3. カート詳細画面の中からでも、個数は自由に変更でき、「更新」ボタンで個数を変更できる。
4. カートから特定の商品を取り出したい場合は、その商品の「削除」ボタンを押す
```
というものにする

- メインとなるビューは以下に挙げている３つであり、細かくは書かないが、それぞれのページの機能をまとめておく

```
# home#index
  - 最初に表示されるページ。
  - なくてもいいが、もし作るならproducts#indexへのリンクを貼る

# products#index
  - 商品一覧。
  - それぞれの商品に「購入数入力欄」と「決定ボタン」とを作る。
  - ボタンを押すとcarts#add_itemへとPOSTアクションを飛ばし、購入数と商品IDを飛ばしたあと、carts#showへとリダイレクトするようにする。

# carts#show
  - カート詳細画面
  - 現在カートに入っている商品一覧（なければ空っぽの表示）をだす
  - 商品が入っている場合は、個数の変更（carts#update_item）、及び商品の取り消し(cart_item)ができるようにする。
  - 合計金額、お支払いボタンがあるとなおいいかも（今回は説明していない）
```

## コントローラの作成

```ruby
class ProductsController < ApplicationController
  # 商品一覧画面
  def index
    @products = Product.all
  end
end
```
```ruby
class CartsController < ApplicationController
  before_action :setup_cart_item!, only: [:add_item, :update_item, :delete_item]

  def show
    @cart_items = current_cart.cart_items
  end
  
  # 商品一覧画面から、「商品購入」を押した時のアクション
  def add_item
    if @cart_item.blank?
      @cart_item = current_cart.cart_items.build(product_id: params[:product_id])
    end

    @cart_item.quantity += params[:quantity].to_i
    @cart_item.save
    redirect_to current_cart
  end

  # カート詳細画面から、「更新」を押した時のアクション
  def update_item
    @cart_item.update(quantity: params[:quantity].to_i)
    redirect_to current_cart
  end

　# カート詳細画面から、「削除」を押した時のアクション
  def delete_item
    @cart_item.destroy
    redirect_to current_cart
  end

  private

  def setup_cart_item!
    @cart_item = current_cart.cart_items.find_by(product_id: params[:product_id])
  end
end
```


# まとめ
- 必要最低限必要なモデルは「商品」と「カート」、それらを紐づけて個数を記録する「カート内商品」モデルであること。
- カートモデルはsessionで管理し、カートに入っている商品の増減の管理はcartコントローラーにまとめて置くとみやすいこと