RailsでECサイトのカート機能を実装する(ユーザーログインあり・なし両方対応)

5 min read読了の目安(約4600字

環境

Ruby 2.6.6
Rails 6.0.3

実装機能

簡単なECサイトのカート機能を作成します。
ログインユーザーにはユーザーに紐づいたカート情報を呼び出し、未ログインユーザーにはsessionを利用します。

前提

ユーザーのログインのためにdeviseを導入しています。 (モデル名はUser)

モデル

ec.jpeg

※Userモデルは省いています
カートモデルを作成することでカート内アイテムを管理しています。

class CreateCartItems < ActiveRecord::Migration[6.0]
  def change
    create_table :cart_items do |t|
      t.integer :quantity, default: 0
      t.references :product, null: false, foreign_key: true
      t.references :cart, null: false, foreign_key: true
      t.timestamps
    end
  end
end


class CreateCarts < ActiveRecord::Migration[6.0]
  def change
    create_table :carts do |t|
      # sessionで管理する場合user_idはnullになるのでnull: falseは不要
      t.references :carts, :user, foreign_key: true
      t.timestamps
    end
  end
end
class Cart < ApplicationRecord
  has_many :cart_items, dependent: :destroy
end

class CartItem < ApplicationRecord
  belongs_to :product
  belongs_to :cart

  # カート内の商品合計に利用
  def sum_of_price
    product.price * quantity
  end
end

class User < ApplicationRecord
  has_one :cart, dependent: :destroy
end

現在のカートを参照するメソッドの定義

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

  helper_method :current_cart
  
  def current_cart
    if current_user
      # ユーザーとカートの紐付け
      current_cart = current_user.cart || current_user.create_cart!
    else
      # セッションとカートの紐付け
      current_cart = Cart.find_by(id: session[:cart_id]) || Cart.create
      session[:cart_id] ||= current_cart.id
    end
    current_cart
  end
end

ユーザーがログインしている場合、紐づいたカートを参照し、なければ作成
ユーザーがいない場合、sessionからカートを参照しなければ作成しています。

ルーティング設定

Rails.application.routes.draw do
  .
  .
  .  
  get '/my_cart' => 'carts#my_cart'
  post '/add_item' => 'carts#add_item'
  post '/update_item' => 'carts#update_item'
  delete '/delete_item' => 'carts#delete_item'
end

他のユーザーのカートを見る必要がないため、showアクションを使わずにカートの中身を見るページを作成しています。

コントローラーの作成

class CartsController < ApplicationController
  before_action :setup_cart_item!, only: %i[add_item update_item delete_item]

  # カート内アイテムの表示
  def my_cart
    @cart_items = current_cart.cart_items.includes([:product])
    @total = @cart_items.inject(0) { |sum, item| sum + item.sum_of_price }
  end

  # アイテムの追加
  def add_item
    @cart_item ||= current_cart.cart_items.build(product_id: params[:product_id])
    @cart_item.quantity += params[:quantity].to_i
    if  @cart_item.save
      flash[:notice] = '商品が追加されました。'
      redirect_to my_cart_path
    else
      flash[:alert] = '商品の追加に失敗しました。'
      redirect_to product_url(params[:product_id])
    end
  end

  # アイテムの更新
  def update_item
    if @cart_item.update(quantity: params[:quantity].to_i)
      flash[:notice] = 'カート内のギフトが更新されました'
    else
      flash[:alert] = 'カート内のギフトの更新に失敗しました'
    end
    redirect_to my_cart_path
  end

  # アイテムの削除
  def delete_item
    if @cart_item.destroy
      flash[:notice] = 'カート内のギフトが削除されました'
    else
      flash[:alert] = '削除に失敗しました'
    end
    redirect_to my_cart_path
  end

  private

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

補足としましては、my_cartアクションでは先にカート内商品の合計金額を計算しております。
(他にきれいな方法があるかもしれません…誰かわかる方教えてください!)
他のアクションでは基本的にparamsに渡したproduct_idを元に、カート内の商品に変更を加えていくといった感じです。

ビューの作成(カートのみ)

app/views/carts/my_cart.html.erb

<h2>カート内アイテム</h2>
<%= render partial: "carts/cart_item", collection: @cart_items, as: "cart_item"%>
<h2><%= "合計:#{@total}円" %></h2>

app/views/carts/_cart_item.html.erb

<li class="list-none">
    <%= cart_item.product.name %>
    <%= cart_item.product.price %>
    <%= form_with(url: update_item_url, method: :post,local: true) do |f| %>
    <%= f.hidden_field :product_id, value: cart_item.product.id %>
    <%= f.number_field :quantity ,value: cart_item.quantity%>
    <%= f.submit "更新", class: "" %>
    <% end %>
    <%= button_to "削除", delete_item_path(product_id: cart_item.product.id), method: :delete,
                                       data: { confirm: "商品を削除しますか?" } %>
</li>

最低限のビューですが、カート内の商品を編集できるようになりました。
タイトルなし.gif

課題

ユーザーと紐づいたカートモデルは決済やログアウトのタイミングで削除すれば良いですが、sessionで管理しているカートモデルのデータを削除するタイミングが決済以外ないので、rake taskで定期的に削除した方がいいかもしれません😗
「コードこうした方がええ感じになるよ」などあれば是非コメントください!