0. はじめに
※こちらの記事は前後篇の前編です!後編はこちらをクリック!!!
*本記事ではLaravel 5.8での開発を解説しています
ECサイトの作成をしていてこんな事を感じた経験はないでしょうか。
「セッションを用いたカート機能実装の実装が難しすぎる!」
過去の自分や現在進行形で困っている方へ向けて
その悩みを解決するためカート機能実装の網羅的な解説記事を作りました。
お役に立てれば幸いです!
前後編の長文となりますのでご了承下さい。
**※追記:完全に FatController です。 恥ずかしいですが、生まれて初めての一から作成したプログラムということでこのままにしておきます。 時間見つけてリファクタリングします。**
目次
-
0.はじめに
- 本記事の対象になりうる方
- 概要
- 1.各分野で必要となる基礎知識
- 2.要求定義と要件定義
- 3.機能設計
- 4.大まかな流れ
- 5.実際の挙動と画面遷移のと処理の流れ(Gif動画)
-
6.関連コード
- ・View -商品情報画面-
- ・View -カートリスト画面-
- ・Route
- ・Controller
-
7. 各実装の詳細説明
-
7-1 解説-addCart-メソッド
- 同一商品をカートに追加する際の個数合算処理
- 最初のif文での処理が終わった後は何をしているのか
-
7-1 解説-addCart-メソッド
-
参考にさせていただいたサイト
-
- ↑ここまでが前編
- ↓これ以降は後編
-
- 7-2 ※解説の前の補足事項 〜ModelとEloquent、DB設計、リレーションの取得〜 - Eloquent(エロクエント) - ORM - Model - DB設計について - マイグレーションとは? - リレーションとは?
- 7-3 解説-index-メソッド
- foreach文で多次元連想配列の内容を追加・変更する(リファレンス渡し)
- Laravelでviewに値を渡すやり方
- N+1問題の解消
- EagerLord
- EagerLordの種類
- 7-4 解説-remove-メソッド
- array_udiff関数で多次元連想配列の加工
- 7-5 解説-store-メソッド
- t_ordersテーブル(注文情報)への保存
- t_order_detailsテーブル(注文詳細情報)への保存
- 複数のテーブルのカラムを紐付ける:外部キー制約
- 参考にさせていただいたサイト
-
本記事の対象になりうる方
✅初学者の方
✅カート機能を実装したことがない方
✅カート実装で詰まっている方
そもそもセッションってなに??
という方は私が作成したスライドを参考にどうぞ(宣伝)概念の理解と全体像の把握に適切です。
~~*HTTPにおけるセッションとは?*~~スライドを作り直しました。
CookieとSession-違いと誕生の背景-
参考
ステートレスとはなにか?
4歳娘「パパ、セッションとCookieってなあに?」
TechRacho/Webアプリのセッション管理とデータ保存を学ぶ#1
概要
以下の機能実装について解説(いずれもセッションを用いて実現)
1. セッションへ商品情報を保存(addCart
メソッド)
2. 商品の注文数の合算(addCart
メソッド)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
※ここまでが前編です。以下は後編で解説
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3. カート画面でカート内の商品情報を表示させる(index
メソッド)
4. カート内商品の個別削除(remove
メソッド)
5. カート内商品をDB内の対応テーブルに保存(store
メソッド)
環境は以下
- macOS Catalina 10.15.7
- Vagrant + VirtualBox + Docker
- CentOS 7.3.1611
- Apache 2.4.6 MySQL 5.7
- PHP 7.3.11 Laravel 5.8
また規約として
PSR-2 コーディングガイド
を採用しています。
1.各分野で必要となる基礎知識の一覧
HTML | PHP | Laravel | Web |
---|---|---|---|
inputタグ | 配列 | requestオブジェクト | ステートレスサーバ |
hidden属性 | 連想配列 | session | アーキテクチャスタイル |
Formタグ | 多次元配列 | bladeテンプレート | REST |
多次元連想配列 | CRUD処理 | Cookie | |
ループ処理 | Eloquent (ORM) | セッション状態 | |
多重ループ処理 | Collection | HTTP | |
array~~関数 | Modelオブジェクト | HTTPメソッド | |
各種の条件分岐 | リファレンス渡し | オブジェクト指向 | |
flag指定 | ドット記法 | ||
真偽値 | MVC+Route | ||
Laravel Collective | |||
Formファサード | |||
デバック方法 |
2. 要求定義と要件定義
要求定義 | 要件定義 |
---|---|
ユーザはカートに商品を入れることができる | 対象商品の商品IDと注文個数を持つオブジェクトをセッションにリスト形式で詰め、それにより商品情報を保存 |
ユーザはカートに入れた商品を削除できる | カート内に存在する指定商品のセッションを削除する |
ユーザはカートに入れた商品を購入できる | DBの対応テーブルに注文商品を保存 |
基本部分のみですが、これだけでも非常に悩みました(´;ω;`)
これだけなんです。たったこれだけなんですが。実際に実装すると…
3.機能設計
*画面には他の機能も実装されていますが本記事では以下の機能のみを取り扱っています*
機能設計 | 機能名 | 処理内容 | 必要データ | 取得元 | ユーザ操作 |
---|---|---|---|---|---|
商品情報画面 | 商品詳細表示/購入個数の決定/カートリストへの商品追加 | ①入力フォームで個数・商品ID・ユーザIDをrequestオブジェクトへ保存 ②同じ商品は個数を合算 ③requestオブジェクトからsessionへ各情報を保存 ④sessionへ保存する値は新規保存と追加更新処理 ⑤カートリスト画面に遷移 | ①商品個数 ②商品ID ③ユーザID | 入力フォーム/inputタグ(hiddein属性) | フォームに個数を入力/「カートへ」をクリック |
カートリスト画面 | ユーザ情報表示/カート内商品の表示/商品の削除と購入 | ①DBからユーザ情報と商品詳細を取得 ②ユーザ情報を表示 ③商品の小計と合計の金額を表示 ④リンクから削除・購入処理の実行 | ①商品個数 ②商品情報 ③ユーザ情報 | sessionから取得/DBから取得 | 「削除」をクリック/「注文を確定する」をクリック |
4.大まかな流れ
①商品詳細画面から商品IDと注文個数をrequestオブジェクトへ保存(addCart
メソッド)
②controllerで上記データをrequestオブジェクトから取り出しセッションへ保存(addCart
メソッド)
**※ここまでが前編です。以下は後編で解説**
③controllerで上記で保存したセッション情報を取得(殆どのメソッド)
④セッションデータを用いてDBから任意の値を引き出す(index
メソッド)
⑤取得したデータから必要な値を取り出す(index
メソッド)
⑥取得した情報をviewに渡す(殆どのメソッド)
⑦カート内商品を個別に削除できるようにする(remove
メソッド)
⑧カート内商品をテーブルに保存(store
メソッド)
5.実際の挙動と画面遷移と処理の流れ(Gif動画)
6.関連コード
・View -商品情報画面-
@extends('layouts.app')
@section('content')
<main>
<div class="container">
<div class="jumbotron bg-white">
<h1 class="text-center">商品情報</h1>
<h3 class="my-4 text-center">
@if(isset($product->product_name))
{{ $product->product_name }}
@endif
</h3>
<div class="offset-sm-3">
<p class="offset-sm-6">
商品カテゴリ:
@if(isset($category_name->category_name))
{{ $category_name->category_name }}
@endif
</p>
<p>商品説明</p>
<p>
@if(isset($product->description))
{{ $product->description }}
@endif
</p>
<p class="mt-4 mb-5">価格:
@if(isset($product->price))
{{ $product->price }}
@endif
円
</p>
</div>
{!! Form::open(['route' => ['addcart.post', 'class' => 'd-inline']]) !!}
{{-- 画面遷移時にPOST送信 session保存に使用 --}}
{{ Form::hidden('products_id', $product->id) }}
{{ Form::hidden('users_id', $user->id) }}
<div class="form-row justify-content-center">
{!! Form::label('prodqty', '購入個数', ['class' => 'mt-1']) !!}
<div class="form-group col-sm-1">
<div class="ml-1">
<input type="number" name="product_quantity" class="form-control" id="prodqty" pattern="[1-9][0-9]*" min="1" required autofocus>
</div>
</div>
{!! Form::label('', '個', ['class' => 'mt-1 mr-3']) !!}
<div class="form-group">
{!! Form::submit('カートへ', ['class' => 'btn btn-primary']) !!}
</div>
</div>
{!! Form::close() !!}
</div>
</div>
</main>
@endsection
・View -カートリスト画面-
@extends('layouts.app')
@section('content')
<main>
<div class="container">
<div class="row">
<div class="col-12 card border-dark mt-5">
<div class="cord-body ml-3 mb-2">
<h4 class="mt-4">お届け先</h4>
<p class="mb-2" style="padding-left: 20px;">
@if(Auth::check())
{{$sessionUser->zipcode}}
{{$sessionUser->prefecture}}
{{$sessionUser->municipality}}
{{$sessionUser->address}}
{{$sessionUser->apartments}}
@endif
</p>
<p style="padding-left: 160px;">
@if(Auth::check())
{{$sessionUser->last_name}}
{{$sessionUser->first_name}}
@endif
様
</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<table class="table mt-5 ml-3 border-dark">
<thead>
<tr class="text-center">
<th class="border-bottom border-dark" style="width:13%;">No</th>
<th class="border-bottom border-dark" style="width:18%;">商品名</th>
<th class="border-bottom border-dark" style="width:15%;">商品カテゴリ</th>
<th class="border-bottom border-dark" style="width:15%;">値段</th>
<th class="border-bottom border-dark" style="width:15%;">個数</th>
<th class="border-bottom border-dark" style="width:15%;">小計</th>
</tr>
</thead>
<tbody>
@foreach ($cartData as $key => $data)
<tr class="text-center">
<th class="align-middle">{{ $key += 1 }}</th>
<td class="align-middle">
{{ $data['product_name'] }}
</td>
<td class="align-middle">
{{ $data['category_name'] }}
</td>
<td class="align-middle">
¥{{ number_format($data['price']) }} 円
</td>
<td class="align-middle">
<button type="button" class="btn btn-outline-dark">
{{ $data['session_quantity'] }}
</button>
個
</td>
<td class="align-middle">
¥{{ number_format($data['session_quantity'] * $data['price']) }}
</td>
<td class="border-0 align-middle">
{!! Form::open(['route' => ['itemRemove', 'method' => 'post', $data['session_products_id']]]) !!}
{{ Form::submit('削除', ['name' => 'delete_products_id', 'class' => 'btn btn-danger']) }}
{{ Form::hidden('product_id', $data['session_products_id']) }}
{{ Form::hidden('product_quantity', $data['session_quantity']) }}
{!! Form::close() !!}
</td>
</tr>
@endforeach
<tr class="text-center">
<th class="border-bottom-0 align-middle"></th>
<td class="border-bottom-0 align-middle"></td>
<td class="border-bottom-0 align-middle"></td>
<td class="border-bottom-0 align-middle"></td>
<td class="border-bottom-0 align-middle">合計</td>
@php
$totalPrice = number_format(array_sum(array_column($cartData, 'itemPrice')))
@endphp
<td class="border-bottom-0 align-middle">
¥{{ $totalPrice }}円
</td>
</tr>
<tr class="text-right">
<th class="border-0"></th>
<td class="border-0">
<a class="btn btn-success" href="{{ route('product_search') }}" role="button">
買い物を続ける
</a>
</td>
<td class="border-0"></td>
<td class="border-0"></td>
<td class="border-0">
{!! Form::open(['route' => ['orderFinalize', 'method' => 'post', $data['session_products_id']]]) !!}
{{ Form::submit('注文を確定する', ['name' => 'orderFinalize', 'class' => 'btn btn-primary']) }}
{!! Form::close() !!}
</td>
<td class="border-0 align-middle"></td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
@endsection
・Route
/*
|--------------------------------------------------------------------------
| カート内商品関連
|--------------------------------------------------------------------------
*/
Route::view('/no-cartList', 'products/no_cart_list')->name('noCartlist');
Route::view('/purchaseCompleted', 'products/purchase_completed');
Route::resource('cartlist', 'ProductController', ['only' => ['index']]);
Route::post('productInfo/addCart/cartListRemove', 'ProductController@remove')->name('itemRemove');
Route::post('productInfo/addCart','ProductController@addCart')->name('addcart.post');
Route::post('productInfo/addCart/orderFinalize','ProductController@store')->name('orderFinalize');
・Controller
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Product;
use App\Category;
use App\Order;
use App\User;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
class ProductController extends Controller
{
/*
|--------------------------------------------------------------------------
| 商品詳細 → カート画面へのSession情報保存
|--------------------------------------------------------------------------
*/
public function addCart(Request $request)
{
//商品詳細画面のhidden属性で送信(Request)された商品IDと注文個数を取得し配列として変数に格納
//inputタグのname属性を指定し$requestからPOST送信された内容を取得する。
$cartData = [
'session_products_id' => $request->products_id,
'session_quantity' => $request->product_quantity,
];
//sessionにcartData配列が「無い」場合、商品情報の配列をcartData(key)という名で$cartDataをSessionに追加
if (!$request->session()->has('cartData')) {
$request->session()->push('cartData', $cartData);
} else {
//sessionにcartData配列が「有る」場合、情報取得
$sessionCartData = $request->session()->get('cartData');
//isSameProductId定義 product_id同一確認フラグ false = 同一ではない状態を指定
$isSameProductId = false;
foreach ($sessionCartData as $index => $sessionData) {
//product_idが同一であれば、フラグをtrueにする → 個数の合算処理、及びセッション情報更新。更新は一度のみ
if ($sessionData['session_products_id'] === $cartData['session_products_id'] ) {
$isSameProductId = true;
$quantity = $sessionData['session_quantity'] + $cartData['session_quantity'];
//cartDataをrootとしたツリー状の多次元連想配列の特定のValueにアクセスし、指定の変数でValueの上書き処理
$request->session()->put('cartData.' . $index . '.session_quantity', $quantity);
break;
}
}
//product_idが同一ではない状態を指定 その場合であればpushする
if ($isSameProductId === false) {
$request->session()->push('cartData', $cartData);
}
}
//POST送信された情報をsessionに保存 'users_id'(key)に$request内の'users_id'をセット
$request->session()->put('users_id', ($request->users_id));
return redirect()->route('cartlist.index');
}
/*
|--------------------------------------------------------------------------
| カート内商品表示
|--------------------------------------------------------------------------
*/
public function index(Request $request)
{
//渡されたセッション情報をkey(名前)を用いそれぞれ取得、変数に代入
$sessionUser = User::find($request->session()->get('users_id'));
//removeメソッドでの配列削除時の配列連番抜け対策
if ($request->session()->has('cartData')) {
$cartData = array_values($request->session()->get('cartData'));
}
if (!empty($cartData)) {
$sessionProductsId = array_column($cartData, 'session_products_id');
$product = Product::with('category')->find($sessionProductsId);
foreach ($cartData as $index => &$data) {
//二次元目の配列を指定している$dataに'product〜'key生成 Modelオブジェクト内の各カラムを代入
//&で参照渡し 仮引数($data)の変更で実引数($cartData)を更新する
$data['product_name'] = $product[$index]->product_name;
$data['category_name'] = $product[$index]['category']->category_name;
$data['price'] = $product[$index]->price;
//商品小計の配列作成し、配列の追加
$data['itemPrice'] = $data['price'] * $data['session_quantity'];
}
unset($data);
return view('products.cartlist', compact('sessionUser', 'cartData', 'totalPrice'));
} else {
return view('products.no_cart_list', ['user' => Auth::user()]);
}
}
/*
|--------------------------------------------------------------------------
| カート内商品の削除
|--------------------------------------------------------------------------
*/
public function remove(Request $request)
{
//session情報の取得(product_idと個数の2次元配列)
$sessionCartData = $request->session()->get('cartData');
//削除ボタンから受け取ったproduct_idと個数を2次元配列に
$removeCartItem = [
['session_products_id' => $request->product_id,
'session_quantity' => $request->product_quantity]
];
//sessionデータと削除対象データを比較、重複部分を削除し残りの配列を抽出
$removeCompletedCartData = array_udiff($sessionCartData, $removeCartItem, function ($sessionCartData, $removeCartItem) {
$result1 = $sessionCartData['session_products_id'] - $removeCartItem['session_products_id'];
$result2 = $sessionCartData['session_quantity'] - $removeCartItem['session_quantity'];
return $result1 + $result2;
});
//上記の抽出情報でcartDataを上書き処理
$request->session()->put('cartData', $removeCompletedCartData);
//上書き後のsession再取得
$cartData = $request->session()->get('cartData');
//session情報があればtrue
if ($request->session()->has('cartData')) {
return redirect()->route('cartlist.index');
}
return view('products.no_cart_list', ['user' => Auth::user()]);
}
/*
|--------------------------------------------------------------------------
| カート内商品注文確定(DB登録)
|--------------------------------------------------------------------------
*/
public function store(Request $request)
{
//$request->session()->forget('cartData');
$cartData = $request->session()->get('cartData');
$now = Carbon::now();
//オブジェクト生成
$order = new \App\Order;
//指定値をオブジェクト代入
$order->user_id = Auth::user()->id;
$order->order_date = $now;
$order->order_number = rand();
//認証済みのユーザーのみオブジェクトへ保存
Auth::user()->orders()->save($order);
//Qrderテーブルの カラム「order_number」が「$order->order_number」の値を取得
$savedOrder = Order::where('order_number', $order->order_number)->get();
//上記Collectionから id の値だけを取得した配列に変換
$savedOrderId = $savedOrder->pluck('id')->toArray();
//注文詳細情報保存を注文数分繰り返す 1回のリクエストを複数カラムに分けDB登録
foreach ($cartData as $data) {
//注文詳細情報に関わるオブジェクト生成
$orderDetail = new \App\OrderDetail;
$orderDetail->product_id = $data['session_products_id'];
$orderDetail->order_id = $savedOrderId[0];
$orderDetail->shipment_status_id = 1;
$orderDetail->order_quantity = $data['session_quantity'];
$orderDetail->shipment_date = $now;
$orderDetail->save();
}
//session削除
$request->session()->forget('cartData');
return view('products/purchase_completed', compact('order'));
}
/*
|--------------------------------------------------------------------------
| 商品詳細画面
|--------------------------------------------------------------------------
*/
public function show($id)
{
//itemDetail/{id} パラメータのユーザIDを元にDBを検索しModelオブジェクト取得
$product = Product::find($id);
if (!empty($product)) {
//productテーブルのcategory_idを取得、Category.phpを経由し該当idが所有するカテゴリー名を取得する
$category_name = Category::find($product->category_id);
$user = Auth::user();
return view('products.productInfo', compact('product', 'category_name', 'user'));
}
return redirect()->route('noProduct');
}
}
7. 実装方法の解説
これから解説するaddCart
メソッドでは
大まかな流れのうち以下の①、②について解説を致します。
①商品詳細画面から商品IDと注文個数をrequestオブジェクトへ保存
②controllerで上記データをrequestオブジェクトから取り出しセッションへ保存
③controllerで上記で保存したセッション情報を取得
④セッションデータを用いてDBから任意の値を引き出す
⑤取得したデータから必要な値を取り出す
⑥取得した情報をviewに渡す
⑦カート内商品を個別に削除できるようにする
~~⑧カート内商品をテーブルに保存~~~~
コチラはview
からcontroller
に処理がまたぎます。
ー処理の流れー
1.ユーザのリクエストという形で商品情報(商品ID&個数)とユーザIDがPOST送信される
2.「カートへ」ボタンを押すとaddcart.post
という名前のルーティングに処理が走る
3.ProductController
のaddCart
メソッドが実行され、カートリスト画面へ遷移する
*Formファサードを使用してます。詳細はこちらのリンクをどうぞ*
・Laravel Collective(Formファサード まとめ) ・LARAVEL COLLECTIVE
・『Laravel Collective』でのHTML生成を簡単にまとめてみる(Laravel 5.3)
7-1 ❏解説-addCartメソッド-❏
ーaddCartメソッドの処理内容ー
- 入力フォームでrequestオブジェクトに保存された商品個数・商品IDを変数に配列で格納
- 上記の変数をsessionに保存
・保存する値は新規保存と追加更新の場合で処理を分ける
・requestオブジェクトからsessionへ各情報を保存
・同じ商品は個数を合算
3.request内のユーザIDを取得し、sessionへ保存
4.カートリスト画面に遷移
(抜粋)
{{ Form::hidden('products_id', $product->id) }}
{{ Form::hidden('users_id', $user->id) }}
<input ~~name="product_quantity"~~>
-
request
に商品ID・ユーザID・商品個数が保存される-
Form::hidden('products_id', $product->id)
第一引数にname属性、第二引数に値 - ➡
$product
というオブジェクトの中のid
というプロパティの値を - ➡
products_id
というname属性でPOST送信
-
--$productのデバック結果--
App\Product {#1197 ▼
#table: "m_products"
#attributes: array:10 [▼
"id" => 1 $product->idはこの値とイコールになる.つまり[1]のこと
"product_name" => "雪の恋人"
"category_id" => 1
"price" => 1980
"description" => "北海道に訪れた恋人たちの想い出"
"sale_status_id" => 1
"product_status_id" => 1
"regist_data" => "2021-02-01 02:06:49"
"user_id" => 1
"delete_flag" => ""
]
ここで送信された値は、後ほどProductController.php
のaddCart
メソッドで取得します
(抜粋)
{!! Form::open(['route' => ['addcart.post', 'class' => 'd-inline']]) !!}
~~
{!! Form::submit('カートへ', ['class' => 'btn btn-primary']) !!}
~~
{!! Form::close() !!}
- 「カートへ」ボタンをクリックすると下記の
web.php
内のaddcart.post
という名前のルーティングによりaddCart
メソッドへ経路案内される.
Route::post('productInfo/addCart','ProductController@addCart')->name('addcart.post');
Route::HTTPメソッド('url','コントローラー名@メソッド名')->name('このルーティングの名前');
抜粋
public function addCart(Request $request)
{
dd($request);
//変数の初期化
$cartData = [];
//商品詳細画面のhidden属性で送信(Request)された商品IDと注文個数を取得し配列として変数に格納
//inputタグのname属性を指定し$requestからPOST送信された内容を取得する。
$cartData = [
'session_products_id' => $request->products_id,
'session_quantity' => $request->product_quantity,
];
- それでは
$request
の中身を見るためにデバッグしてみましょう(ddの部分です)
Illuminate\Http\Request {#43 ▼
~~
+request: Symfony\Component\HttpFoundation\ParameterBag {#44 ▼
#parameters: array:4 [▼
"_token" => "fRtHUgMccP9BnLg5ljSjgfP4XmuzWiM0fnSsI2lK"
"products_id" => "1"
"users_id" => "1"
"product_quantity" => "1"
]
}
この様にrequestの中に各種値が保存されています。この値はproductInfo.blade.php
のform
タグ内でPOST送信された値です。連想配列の形で格納されています。
Form::hidden('products_id', $product->id)
の第一引数であるname属性の値が「key
」に、第二引数の値は$product
というオブジェクトの中のid
というプロパティの値が対応しています。
<input ~~name="product_quantity"~~>
はこのinput
タグに入力した個数が値となります。上記では商品個数を「1」と入力しています。
- 「
->
」をアロー演算子と呼びます ➡ オブジェクトの持つプロパティにアクセスする演算子 - 「
=>
」をダブルアロー演算子と呼びます ➡ 連想配列で使用 「key
」と「値」を紐付ける演算子
**リクエストの取得について(詳細はここをクリック)**
このメソッドでは、「Request」と呼ばれるクラスのオブジェクトが引数に用意されています。これは、リクエスト(ユーザーからのアクセス)に関する各種の情報を管理するクラスです。 引数に渡されたこのRequestからメソッドを呼び出すことで、さまざまな情報を得ることができます。 ```addCart(Request $request)``` これは```$request```を引数として受け付ける際に、```Request```という型で受け取るという宣言です。これはタイプヒンティングと言います。このRequestは上記で書いた ```use Illuminate\Http\Request;``` のことを表しています。 ✔引数としてRequeatクラスそのものを持たせ、 ✔そのクラスから生成されるオブジェクトを$requestという変数に格納。 この場合の型指定は **「Requeatクラスから生成された、オブジェクトだけを引数として指定して下さい。それ以外の値を引数とすることは認めません」** という意味を示します。 「特定のクラスのオブジェクト以外が放り込まれることを禁止する」ことでメソッドが誤った動作をしないように、メソッドの使われるべき形を指定する場合によく使われるのが型指定(タイプヒンティング)です **参考:**[リクエストの取得](https://readouble.com/laravel/5.8/ja/requests.html)抜粋
public function addCart(Request $request)
{
//商品詳細画面のhidden属性で送信(Request)された商品IDと注文個数を取得し配列として変数に格納
//inputタグのname属性を指定し$requestからPOST送信された内容を取得する。
$cartData = [
'session_products_id' => $request->products_id,
'session_quantity' => $request->product_quantity,
];
$request->products_id
というように、
request
からパラメータ名のプロパティを取り出すだけで値を取得できます。
その値をsession_products_id
というkey
名に対応させます。
'session_quantity' => $request->product_quantity
も同様です。
また、商品IDと商品個数は紐付いた情報にしたいので配列として変数に格納します。
そうでないと後々の処理の中で商品と個数を連動させた処理がものすごく面倒になります。
同一商品をカートに追加する際の個数合算処理
//sessionにcartData配列が「無い」場合、商品情報の配列をcartData(key)という名で$cartDataをSessionに追加
if (!$request->session()->has('cartData')) {
$request->session()->push('cartData', $cartData);
} else {
sessionにcartData配列が「無い」場合
➜つまりカート内に商品がない状態を指します。
has();
について
入力値の存在チェック
リクエストに値が存在するかを判定するには、hasメソッドを使用します。hasメソッドは、リクエストに値が存在する場合に、trueを返します。
Laravel 5.8 HTTPリクエスト
if (!$request->session()->has('cartData'))
の意味は、もしsession
にcataData
という名前のデータが存在しない場合、
$request->session()->push('cartData', $cartData);
➜ session
にcataData
という名前で$cartData
を保存する、という意味です。
$cartData
の中身は先ほど説明した配列が入っています。この変数はカートに商品を追加する度に、つまり「カートへ」ボタンをクリックする度に変数に格納されている値が変更されます。
(商品IDと商品個数でしたね)
セッションのデータ保存方法はput
メソッドとpush
メソッドの2種類あります。
put
は「上書き」のような挙動をし、push
は元のデータの上に「追加」されます。
put
は先に入っていた商品を、後に追加した商品で上書きしてしまうので
「商品カートに追加する」という動作に適していません。
カート内に商品を「追加」する機能を実現するにはpush
を用いる必要があります。
配列セッション値の追加
pushメソッドは新しい値を配列のセッション値へ追加します。
たとえばuser.teamsキーにチーム名の配列が含まれているなら、新しい値を次のように追加できます。
$request->session()->push('user.teams', 'developers');
Laravel 5.8 HTTPリクエスト
セッションデータはどこに入っているのか?これは先程したようにdd($request);
を行うと見ることが出来ます。
Illuminate\Http\Request {#43 ▼
~~
#session: Illuminate\Session\Store {#1167 ▼
#id: "QDrE68leWLu3WoDrXb5ca9QlVjXlaF9bi6SkELPs"
#name: "laravel_session"
#attributes: array:7 [▼
"_token" => "7e0aWUKUZYd5nPOIy8iOEtb9EgA0UFTOv99BtWa0"
"_previous" => array:1 [▶]
"_flash" => array:2 [▶]
"url" => []
"login_web_59ba36addc2b2f9401580f014c7f58ea4e30989d" => 1
"cartData" => array:2 [▼ この"cartData"が push でセッションに保存された値です.
0 => array:2 [▼ $cartData の配列が追加されています
"session_products_id" => "2" 構造は多次元連想配列です.
"session_quantity" => "1" 連想配列("cartData" => array:2 一次元目)
] の中に配列(0 => array:2&1 => array:2 二次元目)が
1 => array:2 [▼ その配列の中に連想配列("session_products_id" => "2" 三次元目)が
"session_products_id" => "4" という形になります.
"session_quantity" => "1"
]
]
"users_id" => "1" ユーザIDは put メソッドで上書き保存されていますので
] 商品を何度カートに追加しても値は一つのままです.
#handler: Illuminate\Session\FileSessionHandler {#1168 ▶}
#started: true
}
この配列は"cartData"
をroot
とした三次元連想配列となります。
参考:木構造
これでは初回カート商品追加時の処理が終わりました。
次は同一商品を追加する際、個数を合算して表示するための処理を解説します。ちなみにこの処理がないとどうなると思いますか??
Illuminate\Http\Request {#43 ▼
~~
"cartData" => array:2 [▼
0 => array:2 [▼
"session_products_id" => "2"
"session_quantity" => "1"
]
1 => array:2 [▼ この様に商品ID情報が消え個数の情報だけが残ります.
"session_quantity" => "3" 後に商品IDを用いてDBから商品の詳細情報を取得する必要があります.
] Eloquent でDBから情報を取得する場合,取得することが出来せん.
] 何の個数なのか不明です
}
解説の続きです
//sessionにcartData配列が「無い」場合、商品情報の配列をcartData(key)という名で$cartDataをSessionに追加
if (!$request->session()->has('cartData')) {
$request->session()->push('cartData', $cartData);
} else {
//sessionにcartData配列が「有る」場合、情報取得
$sessionCartData = $request->session()->get('cartData');
dd($sessionCartData);
array:2 [▼
0 => array:2 [▼
"session_products_id" => "2"
"session_quantity" => "1"
]
1 => array:2 [▼
"session_products_id" => "4"
"session_quantity" => "3"
]
]
「同一商品を追加する際、個数を合算して表示するための処理」ですがその前に、最初に行った「セッションに保存した値」を取得する必要があります。
get('cartData');
でkey
を指定すれば値が取得できますね。$sessionCartData
に取得したセッションの値を格納します。
配列の基本ですので意味がわからない場合は「php 配列 要素 取得」とでも検索してみて下さい。
次が「同一商品を追加する際、個数を合算して表示するための処理」になります。
//isSameProductId定義 product_id同一確認フラグ false = 同一ではない状態を指
$isSameProductId = false;
foreach ($sessionCartData as $index => $sessionData) {
//以下の処理はproduct_idが同一であれば、フラグをtrueにする → 個数の合算処理、及びセッション情報更新。更新は一度のみ
if ($sessionData['session_products_id'] === $cartData['session_products_id'] ) {
①$isSameProductId = true;
②$quantity = $sessionData['session_quantity'] + $cartData['session_quantity'];
//cartDataをrootとしたツリー状の多次元連想配列の特定のValueにアクセスし、指定の変数でValueの上書き処理
③$request->session()->put('cartData.' . $index . '.session_quantity', $quantity);
④break;
}
$sessionCartData
の中身はデバックで見た通り多次元連想配列構造をしています。
そういった構造はforeach
でループ処理を行うことで深い次元のkey
や値を取得できます。
繰り返し処理について
```foreach ($変数A(配列の1次元目) as $変数Aの中の配列(2次元目)のkye => $変数Aの中の配列(2次元目)の値)```となります。一回のループ処理では一次元下の配列にアクセスできます。もし、更にその下の次元(上記の例でいえば三次元目)にアクセスしたい場合は多重ループ処理が必要になり、```foreach```の下に更に```foreach```を記述したりと手間がかります。 それを避けたいのであれば、二次元目の配列を抽出するような形で情報を加工しそのデータで繰り返し処理すれば問題有りません。後々その処理が出てきます。 そもそも処理するデータの量が増えるにつれてパフォーマンス的な部分で大きな負担が発生しそうな処理に感じます。なんというかスマートじゃないですよね、実務を知らないのでなんとも言えませんが無駄が多そうです。避けたほうが良いのでしょう、勘ですが。だれか偉い人が居たら指摘して下さい。泣いて喜びます(他力本願)。foreach ($sessionCartData as $index => $sessionData)
は$sessionCartData
の繰り返し処理ですが$index
は配列でれば二次元目のindex
番号を指定し、連想配列であればKey
を指定します。両者の違いは、「番号」か「名前」なのかの違いでしか無いと解釈しています。
$sessionData
は二次元目の値を指定します。その次は条件分岐です。
条件:「カート内の商品とカートに追加される商品のIDが同一であれば以下の処理を実行する」
$sessionData['session_products_id'] === $cartData['session_products_id']
$sessionData['session_products_id']
はもともとカート内にあった商品のID ➡ session
に保存済みの値
$cartData['session_products_id']
は、カートに追加されようとしている商品ID ➡ request
内にある値、session
未保存
それでは処理を順番に説明します
①flag
をtrue
にする ➡ 商品IDが同一で有ること意味する状態に$flag
を書き換える。
②$quantity
に商品IDが同一な商品の個数を合算させた値を格納する。
③②で作成した変数を用いて、セッション内の配列内のデータ(商品個数のみ)を上書きする
④処理を一回きりにする。この処理をしないと、足し算が延々と繰り返されるはめになるやもしれませんぞ。
以下では繰り返し処理の1週目で商品IDが===
なので②の合算処理が始まります。
2周目では商品IDが"4"と"1"なので条件に当てはまらないので処理は実行されません。
dd($cartData); dd($sessionCartData);
array:2 [▼ array:2 [▼
0 => array:2 [▼ 0 => array:2 [▼
"session_products_id" => "2" "session_products_id" => "2"
"session_quantity" => "1" "session_quantity" => "6"
] ]
1 => array:2 [▼ 1 => array:2 [▼
"session_products_id" => "4" "session_products_id" => "1"
"session_quantity" => "3" "session_quantity" => "7"
] ]
] ]
①はそのままですね。元々定義されていたfalse
をtrue
にするだけです。
②は$変数[index or key]
という形です。
この[]で二次元目のindex or key
を指定し二次元目の配列にアクセスし値を取得しています。
記述されている意味は条件の部分と同じです。値が個数なのと 値が足し算されているだけです。
③に関しては個人的に激づまりポイントでした。
ここでどうしても出来なかったこと(余談)
**「多次元配列の中の特定の次元にある、特定の値だけを削除する or 上書きする」** しかも条件分岐内にある条件を満たした値と紐づく配列を自動で処理してくれなければなりません。 つまりこの繰り返し処理の中で **「多次元配列の中の特定の次元にある、特定の値だけを削除する or 上書きする」** を行わなければ再現性の期待できないのです。特定の値とは勿論、商品個数のことです。 色々と試行錯誤しましたが全て失敗に終わりました…。 リファレンスにもその方法は記載されておらず非常に時間がかかりました。$request->session()->put('cartData.' . $index . '.session_quantity', $quantity);
この記述で大事なのはこの記述です
.
そうこの「ドット」こそが重要なのです。ドット記法というのですが、PHPの文字列結合に似ていますよね。
参考 :【PHP】ドット連結記法で配列に値をセット出来るメソッド。laravelのコアファイルより発掘
ドット記法というのは**「深い階層にアクセス」するために用います。
URL(viewメソッド)・config・リレーション・配列・session などで使用できることを確認しています。
多次元配列はツリー構造となっています。
このドット記法**を用いるとツリー構造でいうブランチを辿ることが可能になります。結果ノードにアクセスしリーフを取得できます。
ノードはindex
orkey
に対応しており、リーフはvalue
(値)に対応します。
参考・引用元:木構造
ここでは「root」がcartData
深さ1の「ノード」が 0 つまりindex
番号(連想配列ならkey
)
深さ2の「ノード」として "session_products_id"
「リーフ」としてvalue
(値)である"2"
がそれぞれ対応することになります
dd($cartData); dd($sessionData);
array:2 [▼ array:2 [▼
0 => array:2 [▼ 0 => array:2 [▼
"session_products_id" => "2" "session_products_id" => "2"
"session_quantity" => "1" "session_quantity" => "6"
] ]
1 => array:2 [▼ 1 => array:2 [▼
"session_products_id" => "4" "session_products_id" => "1"
"session_quantity" => "3" "session_quantity" => "7"
] ]
] ]
つまり
$request->session()->put('cartData.' . $index . '.session_quantity', $quantity);
というのは
session
の中のcartData(root)
にアクセスしブランチを辿る
繰り返し処理の一週目が合算処理のための条件(if文条件)を満たすので0($index)
にアクセス
➡ 二周目が満たすなら1($index)
にアクセス
その中にある"session_quantity"
へと到達し$quantity
(6 + 1 = 7)で値をput
(上書き処理)する
という記述になります。
④はそのままです。処理終了です。
再度addCart
メソッドの記述です。
public function addCart(Request $request)
{
//商品詳細画面のhidden属性で送信(Request)された商品IDと注文個数を取得し配列として変数に格納
//inputタグのname属性を指定し$requestからPOST送信された内容を取得する。
$cartData = [
'session_products_id' => $request->products_id,
'session_quantity' => $request->product_quantity,
];
//sessionにcartData配列が「無い」場合、商品情報の配列をcartData(key)という名で$cartDataをSessionに追加
if (!$request->session()->has('cartData')) {
$request->session()->push('cartData', $cartData);
} else {
//sessionにcartData配列が「有る」場合、情報取得
$sessionCartData = $request->session()->get('cartData');
//isSameProductId定義 product_id同一確認フラグ false = 同一ではない状態を指定
$isSameProductId = false;
foreach ($sessionCartData as $index => $sessionData) {
//product_idが同一であれば、フラグをtrueにする → 個数の合算処理、及びセッション情報更新。更新は一度のみ
if ($sessionData['session_products_id'] === $cartData['session_products_id'] ) {
$isSameProductId = true;
$quantity = $sessionData['session_quantity'] + $cartData['session_quantity'];
//cartDataをrootとしたツリー状の多次元連想配列の特定のValueにアクセスし、指定の変数でValueの上書き処理
$request->session()->put('cartData.' . $index . '.session_quantity', $quantity);
break;
}
}
//product_idが同一ではない状態を指定 その場合であればpushする
if ($isSameProductId === false) {
$request->session()->push('cartData', $cartData);
}
}
//POST送信された情報をsessionに保存 'users_id'(key)に$request内の'users_id'をセット
$request->session()->put('users_id', ($request->users_id));
return redirect()->route('cartlist.index');
}
ここまできたら後は戦後処理のようなものですね…長くてすみません(´;ω;`)
最初のif文での処理が終わった後は何をしているのか
最初のif
文で行っていいたことは
- カート内が空なら商品を追加する
- カート内商品と追加しようとする商品のIDが同じなら個数の合算処理
ここからの処理は
3. カートには商品があり、かつ追加しようとしている商品のIDがカート内商品と異なる場合の処理
となります。
//product_idが同一ではない状態を指定 その場合であればpushする
if ($isSameProductId === false) {
$request->session()->push('cartData', $cartData);
}
}
//POST送信された情報をsessionに保存 'users_id'(key)に$request内の'users_id'をセット
$request->session()->put('users_id', ($request->users_id));
return redirect()->route('cartlist.index');
}
①if文
flag
変数を条件として処理を行います。
flag
変数はforeach
前で定義していますね。その後の処理でture
にしていますが、
今回の処理ではtrue
になっていないこと想定した処理になります。
つまり$isSameProductId === false
というのは
「product_idが同一ではない状態を指定」というコメントのとおりで処理に関しても「その場合であればpushする」となります。
商品IDが同一でなければsession
に配列としての商品データ(IDと個数)を「追加」しています。
②最後の処理
request
にあるユーザIDをsession
保存
リダイレクトでルーティングを介して次の処理のためのcontroller
へ飛びます。
カート内商品を表示するためのメソッドとなります。
view()とredirect()の違い
・view()はresouces->viewファイルにアクセス ・redirect()はweb.phpのルーティング情報にアクセスRoute::resource('cartlist', 'ProductController', ['only' => ['index']]);
/*
|--------------------------------------------------------------------------
| カート内商品表示
|--------------------------------------------------------------------------
*/
public function index(Request $request)
{
//渡されたセッション情報をkey(名前)を用いそれぞれ取得、変数に代入
$sessionUser = User::find($request->session()->get('users_id'));
//removeメソッドでの配列削除時の配列連番抜け対策
if ($request->session()->has('cartData')) {
$cartData = array_values($request->session()->get('cartData'));
}
if (!empty($cartData)) {
$sessionProductsId = array_column($cartData, 'session_products_id');
$product = Product::with('category')->find($sessionProductsId);
foreach ($cartData as $index => &$data) {
//二次元目の配列を指定している$dataに'product〜'key生成 Modelオブジェクト内の各カラムを代入
//&で参照渡し 仮引数($data)の変更で実引数($cartData)を更新する
$data['product_name'] = $product[$index]->product_name;
$data['category_name'] = $product[$index]['category']->category_name;
$data['price'] = $product[$index]->price;
//商品小計の配列作成し、配列の追加
$data['itemPrice'] = $data['price'] * $data['session_quantity'];
}
return view('products.cartlist', compact('sessionUser', 'cartData', 'totalPrice'));
} else {
return view('products.no_cart_list', ['user' => Auth::user()]);
}
}
※こちらの記事は前後篇の前編です!後編はこちらをクリック!!!
-
- 各実装の詳細説明
- 1. セッションへ商品情報を保存(addCartメソッド)
- 2. 商品の注文数の合算(addCartメソッド)
-
- ※※以降は後編です
-
- 3. カート画面でカート内の商品情報を表示させる(indexメソッド)
- 4. カート内商品の個別削除(removeメソッド)
- 5. カート内商品をDB内の対応テーブルに保存(storeメソッド)
参考にさせていただいたサイト
- Laravelでsession(セッション)を扱う方法【初心者向け】
- これでセッションとクッキーの理解はスッキリ!(Laravel編)
- Laravel 5.8 HTTPセッション
- Ruby / Rails関連 勉強会 2018.09.19Webアプリのセッション管理とデータ保存を学ぶ#1(社内勉強会)
- yohei-y:weblog ステートレスとは何か
- セッションタイムアウト
- Formのインプットに隠し属性を持たせる
- 4歳娘「パパ、セッションとCookieってなあに?」
- Laravelでのセッション操作(まとめ)
- 【Laravel5.8+Stripe13】ショッピングカートの実装
- Laravelでsessionに入っている連想配列の値を更新したい 解決済 投稿 2019/09/16 13:12
- 【Laravel6】セッションの値が多重連想配列の時に特定のkeyに対応するvalueを更新する方法
- Laravel の .env の値は config() 経由で使う
- Laravel 5.4- Delete array from session variable
- WebProgrammingPortalウェブプログラミングポータル
- Qiitaで記事を公開するときに気を付けるべきマナーについて 〜無断でネットや書籍の内容を丸写しするのはやめよう〜
- 初心者のためのLaravel入門 Requestクラスの基本 (1/5)
- Laravel 5.8 HTTPリクエスト
- Laravelでのリクエストデータの取得
- リクエスト(Request)のあれこれ
- リクエスト処理 まとめ
- フォームデータの送信
- php - laravelのセッションから配列を削除するにはどうすればよいですか
- Laravelカート。削除メソッドを実装する方法は?
- PHP 多次元配列 を作成する
- Qiitaで良い記事を書く技術
- TechRacho/Webアプリのセッション管理とデータ保存を学ぶ#1
- 木構造
- 要件定義~システム設計ができる人材になれる記事
- ユースケース駆動開発実践_要件定義
- Laravel Eloquent with リレーション先のリレーションを指定する方法