LoginSignup
33
31

More than 1 year has passed since last update.

【Laravel】誰でも出来る!sessionを使用したカート機能実装の完全解説!! - 後編 -

Last updated at Posted at 2021-02-28

0. はじめに

※こちらの記事は前後篇の後編です!前編はこちらをクリック!!!
*本記事ではLaravel 5.8での開発を解説しています

ECサイトの作成をしていてこんな事を感じた経験はないでしょうか。
「セッションを用いたカート機能実装の実装が難しすぎる!」
過去の自分や現在進行形で困っている方へ向けて
その悩みを解決するためカート機能実装の網羅的な解説記事を作りました。
お役に立てれば幸いです!

前後編の長文となりますのでご了承下さい。

目次

  • 0.はじめに
    • 本記事の対象になりうる方
    • 概要
  • 1.各分野で必要となる基礎知識
  • 2.要求定義と要件定義
  • 3.機能設計
  • 4.大まかな流れ
  • 5.実際の挙動と画面遷移のと処理の流れ(Gif動画)
  • 6.関連コード
    • ・View -商品情報画面-
    • ・View -カートリスト画面-
    • ・Route
    • ・Controller
  • 7. 各実装の詳細説明
    • 7-1 解説-addCart-メソッド
      • 同一商品をカートに追加する際の個数合算処理
      • 最初のif文での処理が終わった後は何をしているのか
  • 参考にさせていただいたサイト
    • ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    • ↑ここまでが前編
    • ↓これ以降は後編
    • ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    • 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テーブル(注文詳細情報)への保存
      • 複数のテーブルのカラムを紐付ける:外部キー制約
    • 参考にさせていただいたサイト

7-2 ※解説の前の補足事項 〜ModelとEloquent、DB設計、リレーションの取得〜

ここでは今回の記事の理解において必要になると思われる事項についてだけ駆け足で記述しています。
それぞれの話題だけで一記事できてしまいますのでここでの詳細な説明は割愛します。

  • Modelとは、MVCモデルにおけるDB関連の処理を担当するクラス。
  • Laravel 特有のDBデータの取り扱う方法には以下の2種類。他にはSQL文の直書きがあります。

Eloquent ORM
②クエリビルダ  参考:クエリビルダとは?Eloquentの違いと使い方
今回使用するのはEloquentなのでクエリビルダの説明は割愛し、Eloquentの説明のみにします。 

Eloquent(エロクエント)

  • Laravel 独自の ORM(Object Relational Mapping:O/Rマッパー)
    • Laravelで提供されているデータ操作の為の機能
    • データベースとModelを関連付け、柔軟なデータ操作を行う為の Laravel 独自の機能
  • 全てのEloquentモデルは Illuminate\Database\Eloquent\Model を継承する

ORM

  • オブジェクト指向のプログラム言語で作成されたオブジェクトと、リレーショナルDB(MySQLなど)を繋ぐ。
    • データベースのレコードを、プログラミング言語のオブジェクトとして扱えるようにする
  • 「オブジェクト指向のプログラム言語で作成されたプログラム」と「リレーショナルDB」の間には本来互換性がありません。
    • この様な設計思想の異なるシステム間での非互換性をインピーダンス・ミスマッチといいます。ORMはこのインピーダンスミスマッチを解消することが出来ます。
  • データベースからデータを取得したらデータをオブジェクト化します。
    • 逆にPHPのオブジェクトをそのままDBテーブルのデータに変換もする。
  • 互いに関連するデータの集合とそれらに対する手続き群をひとまとめにしたのが「オブジェクト」
    • データと処理のまとまり、ということらしいのですが、ぶっちゃけなんとなくしか理解できません。


交流電気回路分野のインピーダンスとの類似性と考察:(私見)
私自身そういった分野の学問に就学していたので馴染みある概念です。
インピーダンスをかなり大雑把に説明すると、異なる物質間の境界面を波動や波の性質を持つ情報が通過する際に現れる抵抗、もしくはフィルタの様なものです。
交流電気回路に限らず電磁波、光、音響、震動、地震、津波など全ての波、波動現象に適用されうるもので、多くの分野でアナロジが見受けられる概念となります。
ここで重要なのはインピーダンスというのは、伝搬する何か(ここでは情報、プログラム内の命令)の伝わりやすさや、伝搬途中でのベクトルを変化させる度合いを表現する指標だということです。
よく間違われるのですが、伝わりにくさを表す指標ではありません。
伝わる情報そのものの性質、伝搬するために必要な触媒の性質で伝わり易さは変化するからです。
それは物質(システム)ごとで異なるものなので、異なる物質(システム)同士で情報が伝搬する場合、必ずインピーダンス不整合(ミスマッチ)というものが発生します。
即ち、伝えたい情報が正確な形で伝わらなかったり(減衰・増幅)、意味が変わってしまう、といった現象が起きます。
コンピュータの情報のやり取りは状態(電源)のオンオフ、即ち0か1によるものに過ぎません。
その正体は電流もしくは電圧であり、これは性質として「波」なんですよね。
このアナロジが適用できるということは、電気信号と波動という一見別々の性質の様に見える現象が、実は抽象的なレイヤでは同じか非常に近いメカニズムで有ることが示されているんだ、という様に感じます。
いやーこういった別の分野同士をつなげてたり、それによるパラダイムシフトが起きたりするのは胸熱展開ですよね?そうですよね?ええ、そう言ってくれると信じてました。


私は医療従事者なので医療でも適用分野を上げてみたいと思います。
超音波検査で赤ちゃんの画像をディスプレイに描出できるのも、このインピーダンスによって生じる音波の減衰差異を数値化して映像信号に変換しているからなのです。
また水中での音速が空気中の約三倍になるのもインピーダンスの違いで起きうる現象なんですね(更にこの違いを適応した治療法に尿石や結石を超音波で破砕する治療が存在します)
こういった考え方を拡張し適用したのがここで言われる

「インピーダンスミスマッチ」

なのでしょう。
解りやすい例えでは、光の屈折ですね。
光という情報が空気と水といった異なる存在の境界面でベクトルが変化する現象が光の屈折と言われる現象です。
ここでは光を屈折することなく水の中を透過させてあげればいいというわけです。空気がLaravel、水がDBで、光はLaravelからDBへ飛んでくるテーブルデータ取得のためのプログラムですかね。これが屈折されたら困るということです。
この様に情報の入力と出力間で情報が伝わり切るような状態をインピーダンス整合が取れていると表現します。
結局ここで言いたかったのはORMはこのインピーダンス整合のための機能なんですねってことなんですけど、途中から自分の言いたいことんばかり言って熱くなっていしまいましたー!すみません(笑)
ただここで、一つの疑問が生まれます。ほんなら始めからLaravelも水ん中に居たらええんじゃないの?
つまりEloquentやクエリビルダを使用せず、

直接SQL文を直書きすればそもそも「インピーダンスミスマッチ」は起きない

のですよね。
EloquentもSQLの直書きも調べた限りではどちらにもメリット・デメリットあるから場面によって使い分けたらええやんって感じがします。どちらにも原理主義的な意見があるみたいですが、個人的には原理主義的な意見は信用ないようにしています。角度が変われば物事は違う顔をしているのが真理だと思っているからです。
中庸的な意見の参考としてこちらをどうぞ。
一般的な意見としてはこちらを参考にしています。

Model

  • Eloquentでは、モデルクラスを定義しModelというクラスを継承して処理を実装します。
  • Modelとは、MVCモデルの「M」にあたる部分で、主にデータベースとの連携を行う。
  • 命名規則として、テーブル名を 単数形+アッパーキャメル にしたものがモデル名となります。またテーブル名が複数型なのに対して、モデル名は単数型にするという決まりがあります。
    • この関係性が成立する場合に、Laravelではモデルとテーブルが自動的に紐づきます
  • Laravel側でSQL文を記述することなくデータベース内のテーブルのデータを、簡単な記述で取得することが可能
  • 特定のテーブルとModelが1対1になっており、そのモデルの使用をコントローラで宣言することでコントラーラ内でそのModelに紐づくテーブルのデータが取得できます

ex. Modelとテーブルの結合例

Product.php
(抜粋)
class Product extends Model
{
    //対応するテーブルを明示し定義する。 m_products テーブルは商品情報を記録
    protected $table = 'm_products';
}

ex. controllerでのモデルの使用宣言とEloquentでのデータ取得

ProductController.php
抜粋
//モデルの使用宣言
use App\Product;
 //$sessionProductsId はセッションに保存された商品ID番号が代入されている。
 $product = Product::find($sessionProductsId);

$productの中身($sessionProductsIdが「1」だった場合)

  • 返される値はModelオブジェクトとなる
    • アロー演算子(->)で中身のプロパティが取得できる(※->については前編で解説)
    • attributesの中身は連想配列、->の後にkeyを指定し対応する値が取得できる
      • ex.$product->product_nameとすれば値は「雪の恋人」となる
      • ex.$product->descriptionとすれば値は「北海道に訪れた恋人たちの想い出」
App\Product {#142 ▼
  #table: "m_products"
  +timestamps: false
  #fillable: array:3 [▶]
  #connection: "mysql"
  #primaryKey: "id"
  #keyType: "int"
  +incrementing: true
  #with: []
  #withCount: []
  #perPage: 15
  +exists: true
  +wasRecentlyCreated: false
  #attributes: array:10 [▼
    "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" => ""
   ]
  #original: array:10 [▶]
  #changes: []
  #casts: []
  #dates: []
  #dateFormat: null
  #appends: []
  #dispatchesEvents: []
  #observables: []
  #relations: []
  #touches: []
  #hidden: []
  #visible: []
  #guarded: array:1 [▶]
}

$productの中身($sessionProductsIdの中身が配列や複数の値の場合)

  • Collectionオブジェクトが戻り値となる
    • 中身がすべてModelオブジェクトになっている
    • 複数のレコードを返す場合はCollection`で返される
    • getfindなどのメソッドの違いでもCollectionModelか変わる
  • 多次元配列の為Modelオブジェクトとは異なりアロー演算子(->)でプロパティを取得できない
Illuminate\Database\Eloquent\Collection {#1202 ▼
  #items: array:4 [▼
    0 => App\Product {#1205 ▶}
    1 => App\Product {#1206 ▶}
    2 => App\Product {#1207 ▶}
    3 => App\Product {#1208 ▼
      #table: "m_products"
      +timestamps: false
      #fillable: array:3 [▶]
      #connection: "mysql"
      #primaryKey: "id"
      #keyType: "int"
      +incrementing: true
      #with: []
      #withCount: []
      #perPage: 15
      +exists: true
      +wasRecentlyCreated: false
      #attributes: array:10 [▼
        "id" => 5
        "product_name" => "十勝のバターハヤシライス"
        "category_id" => 5
        "price" => 5800
        "description" => "生牛乳から作られたバターをふんだんに使用!10食セット☆"
        "sale_status_id" => 5
        "product_status_id" => 5
        "regist_data" => "2021-02-01 02:06:49"
        "user_id" => 2
        "delete_flag" => ""
      ]

DB設計について

達人に学ぶDB設計 徹底指南書 (Japanese Edition) Kindle 版. からの抜粋

・データベースを使わないシステムは、この世に存在しない。
・最初にデータがある。プログラムはその次にできる。
・データベースを制する者がシステムを制す。データベースは、システムの中心であると同時に、システム開発の中心でもある。

この様にDB設計はアプリケーションの根幹を担うところであると理解していますが、本質的な部分を実感を伴ってここで解説するには自分には全く出来ません。
多くの諸兄の方々がすでに解説記事を出されている所ですのでそちらを御覧ください。
オススメは下記の書籍です。
全く何も知らないと、辛いかもしれませんが体系的に一から学ぶという点ではとても良かったです。
達人に学ぶDB設計 徹底指南書

ここで解っておいてほしい事はDB内の各種テーブルの定義、各種テーブルの同士の関係(リレーション)、テーブル内のデータ管理の仕方などは予め厳密に設計するものであり、それが適正に定められていなければアプリケーションやシステムは期待した機能にはなってくれない、ということです。

下記ではDB設計において、設計がなされた後に、設計を元にDB作成を行う方法を紹介しますが、マイグレーションリレーションについてだけ説明します。他に部分は必要あれば適宜解説をはさみたいと思います。


データベース用語
データベースの用語を理解しよう 「テーブル」「レコード」「カラム」「フィールド」とは?
より引用


Ⅱ.データベースにおける用語
リレーショナルデータベースマネジメントシステム(RDBMS、以降単にデータベースと表記します)は、今日のウェブシステムには欠かせないものになっていて、クライアントの担当者やウェブディレクターでも、データベースについて打ち合わせする機会がよくあります。ウェブにおいてデータベースは、動的なページを生成する際に、テンプレートに差し込むデータを保管し、素早く取り出す役目を担っています。そのデータベースに関してよく使われる用語を整理してみましょう。データベースというとほとんどの人はMicrosoft Excelのような表計算ソフトの画面をイメージするので、今回もExcelとの比較で解説することにします。


Ⅱ-ⅰ.「テーブル」
シートに相当するのがテーブルです。データベースではデータの種類やプログラムの利便性を考慮して複数のテーブルを持つことが多く、Excelのブックのような構造になっているとイメージできます。
図はショッピングサイトの例です。商品部分は商品テーブル、購入者情報は顧客テーブルからというように複数のテーブルを関係(リレーション)させるので、リレーショナルデータベースといいます。
image.png160425_DBword_03.png


Ⅱ-ⅱ.「カラム」
列に相当するのが、”カラム”です。雑誌の囲み記事を”コラム”といいますが、スペルは同じです。データベースではカラムごとに、文字列(と文字数)、数値(と桁数)のように属性が定められるので、打ち合わせにおいては、これら属性のことをカラムと呼ぶ人もいます。


Ⅱ-ⅲ.「レコード」
”テーブル”と”カラム”が、データが保管される場所のことを表していた語に対して、”レコード”はデータそのもののことを指す言葉です。同時に、列である”カラム”に対して行を意味する”ロウ”と同じ意味で用いることもあります。


Ⅱ-ⅳ.「フィールド」
レコードを構成する1つ1つの要素のことです。Excelでいう”セル”に相当します。
カラム、レコード、フィールドの関係を表すと、「複数のレコードの同じフィールドを集めたものがカラム 」ということになります。
また、ユーザーインターフェイスにおいてもフィールドと言う言葉を使います。入力フォームのデータを入力するスペースのこともフィールドといいます。フォームのフィールドから入力されたデータは、(データベースを使っていれば)該当するテーブルのレコード内フィールドにだいにゅされるわけですから、フォームのフィールドとデータベースのフィールドはこの場合同じと言えます。
image.png160425_DBword_04.png

マイグレーションとは?

  • マイグレーションとは、テーブル定義を管理する仕組み
  • 下記の様なER図を元にマイグレーションファイルを作成しマイグレーションを実行します。
    • マイグレーションを実行するとファイル内で定義されたテーブル作成が容易に可能です。
    • ER図通りにファイルを作成し実行するとER図通りのカラムが作成されます。

スクリーンショット 2021-02-19 0.30.58.png
ER図を元に作成したマイグレーションファイル

2020_11_23_101254_create_m_products_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateMProductsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {

        Schema::create('m_products', function (Blueprint $table) {
            // Eloquent にデフォルトで設定されている主キー(Primary Key)はid。主キーを変更する:$primaryKey idを無効にして他のものを主キーとして設定できる。 
            // Eloquent ORMでは主キーに対する規約を設けている。①符号なしINT (unsigned int)②フィールド名はid③オートインクリメント 
            // Laravel5.8 Eloquent:利用の開始 主キー Eloquentは更にテーブルの主キーがidというカラム名であると想定しています。この規約をオーバーライドする場合は、protectedのprimaryKeyプロパティを定義してください。 

            $table->increments('id');
            // increments()で作ったカラムには裏でunsined(符号無し・整数のみ)属性が付与される auto_increment を有効にすると自動で primarykey付与  
            // 1テーブルに対しAUTO_INCREMENTカラム1つのみ。セカンダリーインデックス,またはユニークキーがあるカラムに対して有効。プライマリキー以外でも可 

            $table->string('product_name')->length(64);
            $table->integer('category_id')->unsigned()->index();
            // インデックスを作成することでテーブルとは別に検索用に最適化された状態で必要なデータだけがテーブルとは別に保持される。なのでデータ追加時の処理が重くなるというデメリットもある 

            $table->integer('price')->unsigned()->index();
            // 「UNSIGNED」属性を付加すると、0と正の整数のみを扱う。通常では負の数をカウントするために使用される格納域が
            //正の整数のみをカウントするために使用できるようになり、同じバイト数でより大きな正の整数値を記録できる。データ量が二倍に

            $table->string('description')->length(256);
            // string=varcharタイプ。可変長文字列のことを指す。varchar(m)という形で指定 mはバイト数。0~65535まで char型と異なり、末尾に空白は付かない。・末尾に空白が付いた文字列は
            そのまま格納されるメリット:指定された分だけメモリに格納されるため効率がいいデメリット:文字数が値ごとに違うため処理は遅く不定となる 

            $table->integer('sale_status_id')->unsigned()->index();;
            $table->integer('product_status_id')->unsigned()->index();;
            $table->timestamp('regist_data');
            $table->integer('user_id')->unsigned()->index();
            $table->char('delete_flag')->length(1);
            //固定長文字列のことを指す。・char(m)という形で指定する。mは文字数。0~255まで。・charcter(m)の略。・指定した文字数以下の文字を格納した時、文字列末尾
            //に必要な分の空白を付け加えて指定の長さの文字列として格納するメリット:文字数が固定のため、処理が早く一定。デメリット:文字数が必ず定まった分格納されるため、メモリを圧迫しやすい

            // 外部キー制約とは他のテーブルのデータに参照(依存)するようにカラムにつける制約のこと。参照されるのが親テーブル参照するのが子テーブルと呼ぶ。 
            // 主キーと外部キーはRDBにとって、それぞれのテーブルを関連付けるために使用するとても大切な機能。主キーと外部キーを使った制約で利用した場合、下記の制限が入る

            // 1.存在しない値を外部キーとして登録することはできない 2. 子テーブルの外部キーに値が登録されている親テーブルのレコードは削除できない 
            $table->foreign('sale_status_id')->references('id')->on('m_sales_statuses')->onDelete('cascade');
            $table->foreign('product_status_id')->references('id')->on('m_products_statuses')->onDelete('cascade');
            // CASCADE=親テーブルのレコードに対し、削除または更新を行うと、子テーブル内で同じ値を持つカラムのデータに対して削除または更新を行う 
            // RESTRICT=親テーブルのレコードに対し、削除または更新を行うとエラーとなる。設定を省略した場合 RESTRICT が設定される 
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('m_products');
    }
}

それぞれの型の特徴
・ increments(‘id’) … 「符号なしINT」を使用した自動増分ID(主キー)
・ binary(‘カラム名‘) … バイナリデータカラム
・ boolean(‘カラム名‘) … 真偽値カラム
・ char(‘カラム名‘, 長さ) … 長さを指定する文字列カラム
・ date(‘カラム名‘) … 日付カラム
・ time(‘カラム名‘) … 時間カラム
・ dateTime(カラム名) … 日時カラム
・ double(‘カラム名‘, 桁数, 小数点以下桁数) … ○桁で小数点以下×桁の小数カラム
・ enum(‘カラム名‘, [‘定数‘, ‘定数‘]) … ENUMカラム
・ integer(‘カラム名‘) … 数値データカラム
・ json(‘カラム名‘) … JSONフィールドカラム
・ timestamp(‘カラム名‘) … TIMESTAMPカラム
・ timestamps() … created_atとupdate_atカラム
・ nullableTimestamps() … NULL値を許す以外、timestamps()と同じ
・ string(‘カラム名‘) … VARCHARカラム
・ string(‘カラム名‘, 長さ) … 長さ指定のVARCHARカラム
・ text(‘カラム名’) … TEXTカラム

m_categoriesテーブル
スクリーンショット 2021-02-20 18.01.30.png
m_productsテーブル
スクリーンショット 2021-02-20 18.02.42.png

マイグレーション方法はこちらの記事を参考にして下さい。
Laravel入門 データベーステーブルとマイグレーション
Laravelにおけるマイグレーションのしくみ
Laravelのマイグレーション&スキーマビルダでDBのテーブルやカラムを作成する

リレーションとは?

  • 関連し合う(同じ様な属性のデータ)テーブル同士を繋げさせ、1つの情報(データ)を形成する。これをリレーション(結合)と言います。

商品情報(m_produts)テーブルと商品カテゴリー(m_categories)テーブルをリレーションさせた場合に何が出来るのか?
スクリーンショット 2021-02-19 0.30.58.png

この様なER図で表現されたテーブルのリレーションがあった場合(PK/FKに関しては割愛)

  • 結合に関するパターン(テーブル同士の関係)は、下記のような記述をModelに定義する事でリレーションを行う事が出来ます。
    • これによってproductオブジェクト、及びcategoryオブジェクトをEloquentで取得した際に、両オブジェクトからリレーション先の情報を取得できるようになります。
    • テーブル同士の関係は以下のように多数ありますが説明は割愛します。 Laravel 5.8 Eloquent:リレーション
      • 1対1/1対多/多対多
      • Has One Through/Has Many Through
      • 1対1(ポリモーフィック)/1対多(ポリモーフィック)/多対多(ポリモーフィック)
Product.php
(リレーションメソッドcategoryの抜粋)
class Product extends Model
{
    // Productモデルにcategoryモデルのリレーション設定
    public function category()
    {
        return $this->belongsTo('App\Category');
    }
}
Category.php
(リレーションメソッドproductsの抜粋)
class Category extends Model
{
    // categoryモデルにProductモデルのリレーション設定
    public function products()
    {
        return $this->hasMany('App\Product');
    }
}

リレーションを行いうことで何が出来るのでしょうか?
先程$productの中身を見た時、Modelオブジェクトが格納されていました。
その中の値をアロー演算子で取得できることも説明しましたね。
それと同様に、リレーションを行いうことでリレーション先の値も取得できるよになります。
Eloquentproductオブジェクトを取得するには下記の記述が必要でした。 

$product = Product::find($sessionProductsId);

$sessionProductsIdの中身は複数の配列とします。中身は上記に記載してありますので忘れた方は再確認して下さい。
戻り値がcollectionオブジェクトになりますのでアロー演算子ではプロパティやリレーション先の情報の取得はできません。foreachで回すか、特定の値をヘルパー関数で取得する必要があります。
下記ではforeachでの例を記載します。

$sessionProductsId内のIDで商品情報取得、foreach内で各Productのcategoryを取得

$product = Product::find($sessionProductsId);

foreach ($product as $pro) {
    echo $pro->category->category_name;
}

出力結果:菓子類 果物類 生鮮食品 レトルト類 

①ループ処理によって各ProductModelオブジェクトにアクセス出来るようにします($pro)
②各Modelオブジェクトのリレーション先にアクセスする($pro->category)
categoryオブジェクトにアクセス出来ます。


リレーションオブジェクト取得
 ➡ $pro->category$pro->category()とするとリレーションオブジェクトを取得します。
 ➡ Illuminate\Database\Eloquent\Relations\BelongsToBelongsToオブジェクトです。
 ➡ category()とすることでリレーションメソッドの使用となります。中身は割愛します。
 ➡ このあたりの詳細は後述の 解説-storeメソッド- で触れます。

③リレーション先の値を取得する(->category_name)

これでリレーション先の値の取得ができました!わりかし簡単かと思います。
この他、ここではN + 1問題の発生が問題となります。
この問題に関して該当箇所で詳しく説明したいと思います。

7-3 ❏解説-indexメソッド-❏

大まかな流れのうち以下の③〜⑥について、今から解説致します
①商品詳細画面から商品IDと注文個数をrequestオブジェクトへ保存
②controllerで上記データをrequestオブジェクトから取り出しセッションへ保存
③controllerで上記で保存したセッション情報を取得!
④セッションデータを用いてDBから任意の値を引き出す
⑤取得したデータから必要な値を取り出す
⑥取得した情報をviewに渡す
⑦カート内商品を個別に削除できるようにする
⑧カート内商品をテーブルに保存

ー処理の流れー
1. セッションからユーザIDを取得しDB検索をかける
2. removeメソッドでの配列削除時の配列連番抜け対策
3. セッションそのものではなく、セッションを取得し格納した変数の存在確認
4. true ➡ 必要な要素を抽出(データの加工)し、それを元にDBからデータ取得
 ・上記、3. の変数に格納された配列の中身をループで加工(リファレンス使用)
 ・カートリスト画面へ(viewに情報を渡す)
5. false ➡ 「カート内商品が有りません」画面に遷移

ProductController.php
(抜粋)
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'));
        }
  • セッションから取得した各種情報を元にDBから情報を取得し変数に格納します。

①セッションに保存されているユーザIDで現在ログインしているユーザの情報を取得
②ここが他のメソッドと連動する部分になります
➡ カート内商品を個別に削除するためのメソッドとしてremoveメソッドを作成しています。
詳細は後で後述しますがこのメソッドはセッション内に保存されている商品情報(配列)を削除します。
すると配列のindex番号が歯抜けになる現象が起きます。
具体的には下記の左にある様な配列が有って index番号2を削除したとしましょう。
すると右のような配列構成となります。

array:4 [                                 array:3 [               
  0 => array:2 []                           0 => array:2 []
  1 => array:2 []                           1 => array:2 []
  2 => array:2 []                           3 => array:2 [
  3 => array:2 [                              "session_products_id" => "6"
    "session_products_id" => "6"               "session_quantity" => "1"
    "session_quantity" => "1"                ]
  ]                                        ]
]

2番の配列は削除され、index番号は歯抜け状態になります。
このままでセッションデータを取得、viewに情報を渡しforeachで表示しようとすると下記のエラーが発生します。
空の配列に対して要素の取得を行おうとする、もしくは
配列で存在しない位置のインデックスを参照したときに出るエラーとなります。
スクリーンショット 2021-02-14 15.06.03.png

このエラーを回避するにはこのindex番号の抜けを無くして連番になるようにすれば良いわけです。
この処理をしてくれるのがarray_values();関数になります。

array_values() は、配列 array から全ての値を取り出し、数値添字をつけた配列を返します。

ProductController.php
//removeメソッドでの配列削除時の配列連番抜け対策
if ($request->session()->has('cartData')) {
    $cartData = array_values($request->session()->get('cartData'));
}

このコードの意味は
「セッションの中にcartDataという値が有った場合、その値を取得しindex番号の歯抜けを連番になるように処理した後に、変数に格納する」
になります。
そしてそうではなかった場合の処理は以下になります。

ProductController.php

if (!empty($cartData)) {
   $sessionProductsId = array_column($cartData, 'session_products_id');
   $product = Product::with('category')->find($sessionProductsId);

emptyは変数の中身が空かどうか判定します。空であればtrue判定となります。
!emptyは判定が逆になるようにしています。空であればfalse判定となります。
$cartDataに情報が格納されていればtrueになるように意図しています。
つまり$cartDataに商品情報が格納されていれば以下の処理をして下さい、という記述になります。
参考:PHP 型の比較表

その後の処理は以下になります。
①商品ID「のみ」を変数から抽出
②①を元にDBから商品情報を取得、その際リレーション先の情報も同時に取得

array_column();を用い配列から、必要な値だけを抽出した配列に変換します。
array_column($cartData, 'session_products_id');
第一引数に対象となる「配列」を、第二引数に抽出したい値のkeyを指定します。
ここではセッションに保存された商品IDの値のみで配列を作成しています。デバックで確認してみましょう!

ProductController.php
if (!empty($cartData)) {
   $sessionProductsId = array_column($cartData, 'session_products_id');
   dd($sessionProductsId);


$cartDaraのデバック結果)                    ($sessionProductsIdのデバック結果)
array:4 [                                array:4 [
  0 => array:2 [                           0 => "2"
    "session_products_id" => "2"            1 => "4"
    "session_quantity" => "2"               2 => "5"
  ]                                         3 => "6"
  1 => array:2 [                         ]
    "session_products_id" => "4"          
    "session_quantity" => "3"             商品IDのみの配列が作成されました
  ]                                       indexに名前を付けて連想配列にしたい
  2 => array:2 [                         場合は第三引数を指定して下さい
    "session_products_id" => "5"          詳しくは下記の参考を御覧ください
    "session_quantity" => 2               今回は特に必要無いので配列のままです
  ]
  3 => array:2 [
    "session_products_id" => "6"
    "session_quantity" => "1"
  ]
]

第三引数をしていすると、新しい配列のキー名を指定できます:多次元配列から特定カラムの配列を作る


引数・関数と戻り値、欲しい値の求め方イメージとして料理で捉えると解りやすいかもしれません。料理の材料が引数。関数は全自動調理器です。結果としての戻り値は出来上がった料理になります。関数に引数を渡すと戻り値が返るというのは、食材を全自動調理器にいれると料理ができあがったみたいなイメージで良いと思います。①こういう料理を作りたい(結果)②何で作れる(関数)③必要な材料は?(引数)の順で考えると思考もしやすいし検索もしやすいかもしれません。材料がなければ作れば良いのです。

②①を元にDBから商品情報を取得、その際リレーション先の情報も同時に取得
$product = Product::with('category')->find($sessionProductsId);

  • このコードはProduct.phpというModelを用いて、DBから$sessionProductsId内に存在する商品IDに該当する商品情報を取得(find-探してくる-)
  • その際にProduct.phpというModelを介してCategory.phpというModelからも情報を同時に取得する。with('category')->の部分です。これを「リレーションの取得」といいます。

 
では次にこのindexメソッドの中で一番重要な部分を解説したいと思います。
viewに商品情報を表示させるための処理です。

ProductController.php

            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()]);
        }

foreach文で多次元連想配列の内容を追加・変更する(リファレンス渡し)

ここでしていることは
foreach文で多次元連想配列の内容を追加・変更するです。
解説していきます。

foreach ($cartData as $index => &$data) 

この部分では&によるリファレンス渡しというものを行っています。
コメントアウト部分では「参照渡し」と記載していますが、正確にはPHPの呼び方でリファレンス渡しと呼びます。
参考:リファレンス渡し
まずはこの処理の前後で配列がどう変化したのかを見てもらいましょう

変化前
array:4 [
  0 => array:2 [
    "session_products_id" => "1"
    "session_quantity" => 3
  ]
  1 => array:2 [
    "session_products_id" => "2"
    "session_quantity" => 2
  ]
  2 => array:2 [
    "session_products_id" => "3"
    "session_quantity" => 5
  ]
  3 => array:2 [
    "session_products_id" => "5"
    "session_quantity" => "1"
  ]
]
変化後
array:4 [
  0 => array:6 [
    "session_products_id" => "1"
    "session_quantity" => 3
    "product_name" => "雪の恋人"
    "category_name" => "菓子類"
    "price" => 1980
    "itemPrice" => 5940
  ]
  1 => array:6 [
    "session_products_id" => "2"
    "session_quantity" => 2
    "product_name" => "ハブマンゴー"
    "category_name" => "果物類"
    "price" => 8800
    "itemPrice" => 17600
  ]
  2 => array:6 [
    "session_products_id" => "3"
    "session_quantity" => 5
    "product_name" => "一本マグロ"
    "category_name" => "生鮮食品"
    "price" => 1800000
    "itemPrice" => 9000000
  ]
  3 => & array:6 [
    "session_products_id" => "5"
    "session_quantity" => "1"
    "product_name" => "十勝のバターハヤシライス"
    "category_name" => "レトルト類"
    "price" => 5800
    "itemPrice" => 5800
  ]
]

それぞれの連想配列の要素にproduct_name category_name price itemPrice
keyとした値が代入されていますね。
もともと無かった連想配列を作成し、keyとした値を代入しました。
これがリファレンス渡しになります。

リファレンス渡しを簡単に言うと、変数や配列を上書きするための仕組みになります。
変数と配列は基本的に値渡し、オブジェクトは初めからリファレンス型なので&がなくてもリファレンスされます。

オブジェクトは初めからリファレンス型なので&がなくてもリファレンスされます。

ここは「ちがう!」という意見も拝読した時がありました。正直な所、今の自分では判別できません。
今の所は関係ないので放置してますが、いつ何処でどんな形でこの問題が自分も前に現れるか楽しみです。
参考:PHPのオブジェクトは参照渡しではないし、変数リファレンスは全然別物

「変数の実体はメモリ上に格納された領域」という話があります。
難しい言い方ですが、簡単に言えばコンピュータの記録領域に変数の内容を保存する領域があるという話です。
領域には番号がありメモリ番地といいます。住所みたいなものですね。
・値渡し (call by value) ➡ 変数の値をコピーする渡し方
・参照渡し (call by reference) ➡ 変数が格納されているメモリ領域(番地)を共有する

メモリ上に格納されている変数を変数にコピーするのが値渡し。オリジナルの変数の内容は変更できません
変数の情報が格納されている場所を共有するのがリファレンス渡し。ここにはオリジナルの変数の値が格納されているのでここの内容を変更すればオリジナルの値を変更できます。
イメージですが値渡しが新築で、リファレンス渡しがリフォームです。
新築は他の一軒家の間取りを真似して作られいます。住所も変わるので保存しているメモリ領域(番地)も変わります。
リフォームは場所は変わらず中身を変える感じです。住所も変わらないのでメモリ番地も変わりません。

参考:
Rubyist Magazine -値渡しと参照渡しの違いを理解する-
【PHP】全ボクが実務で泣いた「値渡し」と「参照渡し」を完!全!理!解!

値渡しとリファレンス渡しの違い
(値渡し)                          (リファレンス渡し)
$a = 1;                          $a = 1;
$b = $a;   //変数aを代入           $b = &$a;  //変数aを&をつけて代入
$a = 2;    //変数aの値を変更        $a = 2;    //変数aの値を変更
echo $b;                         echo $b;

出力結果                          出力結果
1                                2


$a = [1, 2, 3];                  $a = [1, 2, 3];
$b = $a;     //配列を代入          $b = &$a;    //配列を代入
$a[1] = 22;  //配列aを変更         $a[1] = 22;  //配列aを変更
dd($b);                          dd($b);

出力結果                          出力結果
array:3 [                       array:3 [
  0 => 1                           0 => 1
  1 => 2 //$b[1]の値は変わらない       1 => 22  //$b[1]の値が変わる
  2 => 3                           2 => 3
 ]                                ]

最後に忘れていけないのはこの部分です!

unset($data);

foreachが終了しても$dataには配列の最後の要素に対する参照が設定されたままのため(3 => & array:6の部分)、その後のプログラムで間違って要素の値を書き換えないように変数の割り当てを明示的に解除する必要があります。
別の用途に$dataを使おうとして、何も考えずに何かを代入すると$arrayの最終要素が上書きされてしまうわけです。
これをしないと、その後のプログラムで値を書き換えてしまい思わぬバグを生む可能性があります。
unsetすると&が外れます。

そしてその後のviewへ値を渡す部分とif文の条件以外だった場合の処理部分

return view('products.cartlist', compact('sessionUser', 'cartData', 'totalPrice'));

        } else {

            return view('products.no_cart_list',  ['user' => Auth::user()]);
        }

まずif文の条件以外だった場合の処理部分の説明です。
もともとの条件として

if ($request->session()->has('cartData'))

がありました。この意味は「カート内に商品があればtrue」ということですので
条件外というのは「カート内に商品がない場合false」とする条件になります。
その場合表示される画面はこちらです。

スクリーンショット 2021-02-21 16.32.28.png

ここで必要な情報はログインユーザ情報なので
return view('products.no_cart_list', ['user' => Auth::user()]);
の部分で画面遷移先を指定し、ログインユーザ情報をviewに渡しています。
ここではセッションのデータからユーザ情報は取得できません。
セッションにユーザ情報を保存するためには商品詳細画面からユーザIDをフォームから送信する必要がありました(前編参照)。
今回、商品がカート内にないという状態ですから、商品IDもユーザIDもrequest オブジェクトの中に存在しないのでそもそもセッションに保存し取得することも出来ません。

viewには「セッション内のユーザIDで取得したユーザ情報」、「カート内の商品情報」、そして「商品の合計金額(小計)」を渡します。
これでBladeファイルで変数として各値を使用することが出来ます。
ここでの渡し方ですが、compactメソッドを使用しています。基本的な配列で渡す方法と比較してみましょう。

Laravelでviewに値を渡すやり方

・配列で渡す

return view('products.cartlist', [
        'sessionUser'=> sessionUser, 
        'cartData'=> cartData, 
        'totalPrice'=> totalPrice, 
    ]);

・compact関数で渡す

return view('products.cartlist', compact('sessionUser', 'cartData', 'totalPrice'));

compact関数の方がシンプルかつ短いですね!
すでに定義した変数と同じ名前の文字列をドット区切りで渡す。 viewに渡す値は、変数名と同じ名前になることが殆どになるかと思うので
compact関数でほとんど良いような気がします。

わざわざ配列で渡したい場合ってcontroller内での変数名とviewでの変数名を別々にした場合に限る気がするのですがどうなんでしょうか?
他の使用方法で有用なものがあれば教えて頂きたいです。

N+1問題の解消

※解説の前の補足事項 〜ModelとEloquent、DB設計、「リレーションの取得」について〜
の最後で述べたN+1問題についてここで詳しく解説したいと思います。

N+1問題の概要
各言語のFWで使用されるORMでは、あるモデルが参照している別のモデル(リレーション関係にあるテーブル)を参照するとその時点でSQLが発行されてしまいます。
これは単純にリレーションしたとしても値にアクセスした時点でデータを取得しに行く仕様からきているものです。
ループのなかでSQLを都度発行するようなコードだとクエリが膨大になり重くなることで、気が付かないうちにパフォーマンスが低下するという問題です。
SQLを実行するという処理が一番システムに負荷をかるのですが、アクセス集中などでサービスが停止したりするのもSQLの実行回数が増え捌ききれなくなりDBサーバが落ちる場合が多いそうです。
よってSQLの発行回数をいかに少なくするかは非常に重要な問題です。

では※解説の前の補足事項 〜ModelとEloquent、DB設計、「リレーションの取得」について〜
で使用したコードをもとに解説してきたいおと思います。

$sessionProductsId内のIDで商品情報取得、foreach内で各Productのcategoryを取得
$product = Product::find($sessionProductsId);

foreach ($product as $pro) {
    echo $pro->category->category_name;
}

この様なコードでSQLが何回発行されているのか?
まずはそれを確認しましょう。
下記のように\DB::enableQueryLog();を上部に
dd(\DB::getQueryLog());を下部に記述しSQLの発行ログをデバッグしてみましょう。

ちなみに「laravel sql 確認」などで検索すると他の方法も確認できますが->toSql();という方法は

toSqlメソッドが定義されているクラスは、Builderクラスになります。
以下のエラーが出ます。
Method Illuminate\Database\Eloquent\Collection::toSql does not exist.

とのことでBuilderクラス以外では使えません。参考:allメソッドを使うとtoSqlメソッドが使えない
今回取得されるのはcollectionオブジェクトなので異なる方法でクエリログを確認しています。

\DB::enableQueryLog();            
$product = Product::find($sessionProductsId);

foreach ($product as $pro) {
    echo $pro->category->category_name;
}
dd(\DB::getQueryLog());

出力結果:菓子類 果物類 生鮮食品 レトルト類


[クエリログ]
array:5 [
  0 => array:3 [
    "query" => "select * from `m_products` where `m_products`.`id` in (?, ?, ?, ?)"
    "bindings" => array:4 [
      0 => "1"
      1 => "2"
      2 => "3"
      3 => "5"
    ]
    "time" => 0.47
  ]
  1 => array:3 [
    "query" => "select * from `m_categories` where `m_categories`.`id` = ? limit 1"
    "bindings" => array:1 [
      0 => 1
    ]
    "time" => 0.82
  ]
  2 => array:3 [
    "query" => "select * from `m_categories` where `m_categories`.`id` = ? limit 1"
    "bindings" => array:1 [
      0 => 2
    ]
    "time" => 0.22
  ]
  3 => array:3 [
    "query" => "select * from `m_categories` where `m_categories`.`id` = ? limit 1"
    "bindings" => array:1 [
      0 => 3
    ]
    "time" => 0.22
  ]
  4 => array:3 [
    "query" => "select * from ```m_categories` where `m_categories`.`id` = ? limit 1"
    "bindings" => array:1 [
      0 => 5
    ]
    "time" => 0.17
  ]
]
]

この様に、N+1問題のN(m_categories)+1(m_products)にそれぞれ対応します。リレーション先(m_categories)を参照する度にSQLを発行しDBにアクセスしています。
これを解消する方法はちゃんと用意されています。

EagerLord

Eloquentでデータを取得後にループで回して値にアクセスしている部分は、その数だけクエリが発行されてることになります。
この、N+1問題を防ぐ為に関連するデータを一括取得を行うのがEagerロードになります。
Eagerロードを行うには、with()メソッドを使います。

全Productを取得し、m_categoryテーブルをEagerLordしておく
$product = Product::with('category')->find($sessionProductsId);

with(リレーションメソッド名)の部分がEagerLoadです。
こうすることで、次の 2 クエリしか発行されないようになります。
N+1問題は
①親モデル(Product.php)のオブジェクト生成時にはリレーション先の情報がなく
②アロー演算子で子モデル(Category.php)を繋げた時(->category)に初めてリレーションデータを取得
③よってリレーションプロパティにアクセスする度にSQLが発行される

以下の様にすれば解決します
「事前にリレーション先の情報を含めた親モデル(Product.php)のオブジェクト生成を行えばいい」

それがEagerLordwith(リレーションメソッド名)です!やることは簡単ですね。


余談:N+1問題におけるORMの重たさについて
以下はこちらのサイトの引用です!N+1問題におけるORMの重たさについて
直接SQLを実行した場合、ORMを利用した場合と比べて、一桁高速です。N+1回のクエリを実行していても、クエリ2回だけのORMよりも高速です。
つまり、この程度の単純なクエリの場合、ORMの処理時間のほとんどはRDBの呼び出しではなく、SQLの組み立てやモデルオブジェクトの構築に費やされているのです。
このような仕組みを理解していれば、この処理をチューニングする場合でも機械的にN+1問題対策として prefetch_related を適用するのではなく、できるだけORMレイヤーの機能を利用せずに必要な値だけを直接取得する、などの手段を思いつくのではないでしょうか。

array:2 [
  0 => array:3 [
    "query" => "select * from `m_products` where `m_products`.`id` in (?, ?, ?, ?)"
    "bindings" => array:4 [
      0 => "1"
      1 => "2"
      2 => "3"
      3 => "5"
    ]
    "time" => 0.48
  ]
  1 => array:3 [
    "query" => "select * from `m_categories` where `m_categories`.`id` in (1, 2, 3, 5)"
    "bindings" => []
    "time" => 0.48
  ]
]

EagerLordの種類

複数のリレーション先情報を取得したい場合→引数を配列に

$product = Product::with(['category', 'user'])->find($sessionProductsId);

リレーション先の、その先のリレーション情報取得(ネスト)→ドット記法

$product = Product::with('category.user')->find($sessionProductsId);

7-4 ❏解説-removeメソッド-❏

ProductController.php
    /* 
    |--------------------------------------------------------------------------
    | カート内商品の削除
    |--------------------------------------------------------------------------
    */
    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()]);
    }

Feb-21-2021 17-36-24.gif

大まかな流れのうち以下の⑦について、今から解説致します
①商品詳細画面から商品IDと注文個数をrequestオブジェクトへ保存
②controllerで上記データをrequestオブジェクトから取り出しセッションへ保存
③controllerで上記で保存したセッション情報を取得
④セッションデータを用いてDBから任意の値を引き出す
⑤取得したデータから必要な値を取り出す
⑥取得した情報をviewに渡す
⑦カート内商品を個別に削除できるようにする
⑧カート内商品をテーブルに保存

ーremoveメソッドの処理内容ー
1.セッション情報の取得
2.削除商品のデータを2次元連想配列(1次元ではなく)として作成
3.array_udiff関数で多次元連想配列の加工
4.加工した多次元連想配列でセッションデータを上書き
5.商品削除によってカート内が空になっていないか判定し処理

ProductController.php
 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]
        ];

①ここはこれまでと同じですね。
②ここは少し特徴があります。配列作成時にわざわざ2次元の連想配列にしています。
これから説明する「3.array_udiff関数で多次元連想配列の加工」に必要な処理だからです。

この値はviewにあるformの削除ボタンから商品IDと個数をrequestオブジェクトに格納した値です。
前編参照)。

cartlist.blade.php
(抜粋)
{!! 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() !!}
web.php
Route::post('productInfo/addCart/cartListRemove', 'ProductController@remove')->name('itemRemove');

array_udiff関数で多次元連想配列の加工

参考:array_udiff
参考:二つの多次元配列を比較して、片方にだけ含まれるものを抽出する方法
array_udiff関数は、自分で作成したfunctionを用いて、引数に当てられた配列に関しての差分を計算してくれます。

ProductController.php
//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;
});

ここでの引数は

array_udiff($sessionCartData, $removeCartItem

functionの内容は

$result1 = $sessionCartData['session_products_id'] - $removeCartItem['session_products_id'];
$result2 = $sessionCartData['session_quantity'] - $removeCartItem['session_quantity'];
    return $result1 + $result2;


実はかなり苦労しました…。


着想もそうですがそれを実現する方法を想像することも難しかったです。

ここで大事だった着想・発想
1.配列の差分を計算する
2.必要な関数を見極める
3.計算結果をどう活かすのか

ここに着手した時点で前編で解説したドット記法で「深い階層にアクセス」する方法を知らなかったので
直接セッションデータを編集する方法を知らなかったのです…。
ドット記法で行えば
$request->session()->forget('cartData.' . $index);
のような感じで直接、削除したいデータにアクセスして削除できたのでしょう…。
あえて苦悩の証として残してます(´;ω;`)
愛着も有るんですよね(笑)

ここでしていることはコメントの通りなのですが
sessionデータと削除対象データを比較、重複部分を削除し残りの配列を抽出
です。

ここでは
・元々セッション内に保存されている商品情報を収めた2次元連想配列

viewでユーザが削除ボタンでリクエストした商品情報、をもとに作成した2次元連想配列

値の「重複部分」を引き算しています。
これは$sessionCartData$removeCartItem双方に格納されたsession_products_idsession_quantityの値が、お互いに両方とも等しい場合を意味しています。
個数だけを見て重複と判断されてしまっては「指定した商品を消したい」という要求が実現しません。

id=1id=2の商品の個数がそれぞれ1だった場合、削除されるべきはどちらになるのでしょうか?
個数の重複だけを基準にしてしまってはどちらが消えるのか解らなくなってしまいます。
なので個数と商品IDの双方が重複している配列を削除出来るように指定しなくてはいけません。
それがこのarray_udiff関数内で指定されている内容になります。

こういった商品IDと個数2つの値が対応し合う形の配列構造だからこそ実装できる処理となります。
この配列構造は商品をカート内に入れるためのメソッドであるaddCartメソッドで定義されています。

また、商品IDと商品個数は紐付いた情報にしたいので配列として変数に格納します。
そうでないと後々の処理の中で商品と個数を連動させた処理がものすごく面倒になります。
参考:本記事の前編

また実際の引き算では更に多次元連想配列を細分化して計算しています。
計算式では1次元の連想配列に分解しています。

$result1 = $sessionCartData['session_products_id'] - $removeCartItem['session_products_id'];
$result2 = $sessionCartData['session_quantity'] - $removeCartItem['session_quantity'];

この様に個々の値を別々に計算しています。
それでは実際の計算結果を見てみましょう。まずは計算のもとになる各配列の中身です。
ここで多次元構造を見て下さい。

②ここは少し特徴があります。配列作成時にわざわざ2次元の連想配列にしています。

と先ほどお話ししました。その理由は以下になります。
①計算の際のデータの取り扱い方を同じに出来るので、同じ多次元構造にしたかった
array_udiff関数は「配列同士の計算にしか使えない」
➡ もし$removeCartItemを作成時に2次元でなく1次元連想配列にした場合、計算時での配列細分化の際に配列の次元が合わなくなる。

dd($sessionCartData);                      dd($removeCartItem);

array:4 [                                 array:1 [
  0 => array:2 [                            0 => array:2 [
    "session_products_id" => "1"               "session_products_id" => "1"
    "session_quantity" => "1"                  "session_quantity" => "1"
  ]                                          ]
  1 => array:2 [                          ]
    "session_products_id" => "2"
    "session_quantity" => "1"
  ]
  2 => array:2 [
    "session_products_id" => "3"
    "session_quantity" => "1"
  ]
  3 => array:2 [
    "session_products_id" => "4"
    "session_quantity" => "1"
  ]
]

ここでの計算は以下の連想配列が対象になります。2つの2次元連想配列の重複部分ですね。
計算式では1次元の連想配列に細分化して計算しています。
$removeCartItemの値はユーザが削除したい商品情報です。

ここで

計算式では1次元の連想配列に細分化して計算しています。

という点をもう少し詳しく解説したいと思います。
細分化せずに変数内に格納された2次元連想配列同士で引き算するとどうなるでしょうか?
$result1 = $sessionCartData - $removeCartItem;
このような形ですね。すると

Unsupported operand types = 「サポートされていないオペランド型」

というエラーが吐かれてしまいました。
オペランド (operand ) とは「被演算子」のことです。

プログラムの式は
$a + $b;
のように書きますが、これを言い換えるとこうなります。
オペランド 演算子 オペランド;
ここから Unsupported operand types というエラーは、式のオペランド同士の組み合わせが良くない、ということがわかります。
参考:[PHP] Uncaught Error: Unsupported operand types

phpで型が違う変数同士の加算で起こるエラーの様ですが、どうも今回違うみたいでした。
左右のデバック内容が全く同じ「変数内に格納された2次元連想配列同士」でも発生したからです…。
他の可能性も考えました。

phpは「動的型付け」といわれる言語で、変数の型が勝手にころころ変わってしまう言語です。
動的型付けの対義語として、静的型付けがあり、例を挙げるとjavaなどです。
以下のコードは、javaでは(基本的に)エラーになり、phpでは当たり前に実現できるものです。

$hoge = 'a/b/c';       //$hogeはstring
$hoge = explode('/', $hoge);  //$hogeはarray ['a','b','c']

javaでも無理やり同じことをするには、Object型として変数を定義の上、そのとき何型とみなすかをその都度キャストにて明示する必要があります。
これは、「静的型付け言語では、変数の型が勝手に変わらないから、プログラマがいちいち手動で変えなければいけない」わけです。

public class test
{
public static void main(String...args)
{
Object hoge = "a/b/c";
hoge = ((String)hoge).split("/");
for(String elem : (String[])hoge)
System.out.println(elem);
}
}

両者を比較してみてください。動的型付けでは、プログラマが型を意識しなくてもコードが書きやすい反面、場合によっては型が勝手に変わって矛盾を生むことによるエラーが起きやすいです。
ご質問における「不思議なこと」の正体は、「型が勝手に変わる」という動的型付けの性質そのものです。
一方静的型付けでは、プログラマが型を意識しなければいけませんが、型が変わることがないので、ご質問のような「不思議なこと」が起こりません。
参考:PHP Fatal error: Unsupported operand types

うーん、今回これでもなさそうだなぁ…と頭をひねっているとリファレンスにこういった記述を見つけました。

データの比較にコールバック関数を用い、配列の差を計算します。 この関数は array_diff() と異なり、 データの比較に内部関数を利用します。

データの比較にコールバック関数を用い、配列の差を計算します。
配列の差を計算します。
配列の差を
「配列」…

「・・・・・・!!!!これや…!!これやったんや答えは!もろたで工藤!」

そうなんです。「配列」なんです、扱えるのは。ここには多次元配列が入っています。エラー出るわけです。
なので細分化して「配列」にしました。以下の形ですね。
$sessionCartData['session_products_id'] - $removeCartItem['session_products_id'];

それでは計算結果を見ていきましょう。以下が計算対象になる連想配列です。

dd($sessionCartData);                      dd($removeCartItem);


  0 => array:2 [                            0 => array:2 [
    "session_products_id" => "1"               "session_products_id" => "1"
    "session_quantity" => "1"                  "session_quantity" => "1"
  ]                                          ]

計算結果です。

dd($removeCompletedCartData);

array:3 [
  1 => array:2 [
    "session_products_id" => "2"
    "session_quantity" => "1"
  ]
  2 => array:2 [
    "session_products_id" => "3"
    "session_quantity" => "1"
  ]
  3 => array:2 [
    "session_products_id" => "4"
    "session_quantity" => "1"
  ]
]

$sessionCartDataindex番号が無くなっています。
これにて計算終了ですがこのままではあきません。

❏解説-indexメソッド-❏での

  1. removeメソッドでの配列削除時の配列連番抜け対策

で行っている処理が必要になるのです。必要があれば遡って確認してくださいませ。


書いてて気づきましたがこの書き方良くないですね
書いてて気づきましたがその処理、ここで上書きする前に実行すべきですね!
単一のメソッド内で処理が完結するし、コードの流れも解りやすいですね!
オブジェクト指向にしろ構造化プログラミングにしろ、この手のコンセプトの目指すところは
「いかにコード間の依存関係を減らして、メンテナンスしやすいプログラムを書くか」
なんだろうなと理解してるのですが、そういった観点からみると「コード間の依存関係」をつくってしまったなと感じますね。

ではそれ以降の処理の部分ですがここはindexメソッドで解説したのとほとんど同じになりますが大切な部分があります。
上記の計算結果である$removeCompletedCartDataでセッション内の
cartaDataを上書き(putメソッド)せねばなりません。その後再取得ですね。


上書きしないとどうなる?(余談)
削除直後は消えるのですが再読み込みすると復活します。
以前Twitterに上げた動画のリンク貼ります。興味あれば見てみて下さい。
ツイートした日は誕生日なのですがめっちゃ苦しんでました。
人生で一番スッキリしない誕生日でした(笑)削除しても復活する商品達(Twitter)

        //上記の抽出情報で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()]);
    }

再取得して、セッションデータがあればindexメソッドへ処理が流れます。
商品がすべてなくなれば、以下の画面を表示します。
スクリーンショット 2021-02-28 17.16.25.png

7-5 ❏解説-storeメソッド-❏

ProductController.php
    /*
    |--------------------------------------------------------------------------
    | カート内商品注文確定(DB登録)
    |--------------------------------------------------------------------------
    */
    public function store(Request $request)
    {
        $cartData = $request->session()->get('cartData');
        $$carbonNow = Carbon::now();

        //オブジェクト生成
        $order = new \App\Order;
        //指定値をオブジェクト代入
        $order->user_id = Auth::user()->id;
        $order->order_date = $now;
        $order->order_number = rand();
        //認証済みのユーザーのみDBへ保存
        Auth::user()->orders()->save($order);

        //Qrderテーブルの カラム「order_number」が「$order->order_number」の最新のレコードを一つ取得
        $savedOrder = Order::where('order_number', $order->order_number)->latest()->first();
        //上記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 = 3;
            $orderDetail->order_quantity = $data['session_quantity'];
            $orderDetail->shipment_date = $now;
            Auth::user()->orderDetails()->save($orderDetail);
        }

        //session削除
        $request->session()->forget('cartData');
        return view('products/purchase_completed', compact('order'));
    }

大まかな流れのうち以下の⑧について、今から解説致します
①商品詳細画面から商品IDと注文個数をrequestオブジェクトへ保存
②controllerで上記データをrequestオブジェクトから取り出しセッションへ保存
③controllerで上記で保存したセッション情報を取得
④セッションデータを用いてDBから任意の値を引き出す
⑤取得したデータから必要な値を取り出す
⑥取得した情報をviewに渡す
⑦カート内商品を個別に削除できるようにする
⑧カート内商品をテーブルに保存

ーstoreメソッドの処理内容ー
1. t_ordersテーブル(注文情報)への保存
1-1.セッション情報の取得
1-2.保存したいデータに対応するモデルのオブジェクトを作成する
1-3.保存したい値をオブジェクトに格納する
1-4.saveメソッドでDBの各テーブルに値を保存
カラム
・ id(主キー)
・ user_id
・ order_date
・ order_number

2.t_order_detailsテーブル(注文詳細情報)への保存
2-1.t_ordersテーブルとt_order_detailsテーブルの指定するカラムを紐付けるた値を取得する
2-2.保存したいデータに対応するモデルのオブジェクトを作成する
2-3.保存したい値をオブジェクトに格納する
2-4.注文詳細情報保存を注文数分繰り返す(foreach内でsaveメソッド実行)
カラム
・ id
・ product_id
・ order_id(外部キー)
・ shipment_status_id
・ order_quantity
・ shipment_date

それでは一つ一つ見ていきましょう。

t_ordersテーブル(注文情報)への保存

public function store(Request $request)
    {
       $cartData = $request->session()->get('cartData');
       $carbonNow = Carbon::now();

        //オブジェクト生成
       $order = new \App\Order;
        //指定値をオブジェクト代入
        $order->user_id = Auth::user()->id;
        $order->order_date = $now;
        $order->order_number = rand();
        //認証済みのユーザーのみDBへ保存
        Auth::user()->orders()->save($order);

①セッション取得しています。
②変数にCarbonオブジェクトを格納し、オブジェクト変数を作成する
③EloquentでDBの対応テーブルへデータを保存する

①はもう説明不要ですね
②日付操作に便利ということでしたが今回この恩恵を感じる場面はなかったのです。
が、使いたかったので使いました!現在の日時が取得できます!

Carbon(カーボン)はPHPに標準実装されているDateTimeクラスを継承したクラスで、Laravelなどのメジャーフレームワークでも採用されている日時を扱うクラスです。CarbonはDateTimeクラスの不便な部分を拡張しているので、時間の比較、時間の加算/減算をより便利に使うことができます。
参考:【第1回】PHPで日付や時刻を扱うときに便利なライブラリ「Carbon」

③については下記の流れ

③-1. 対象となるモデルのオブジェクト生成➡変数に代入(オブジェクト変数という)
③-2. オブジェクトの属性(#attributes)に値を代入  
③-3. saveメソッドで新しいレコードをDBに挿入する()

モデルオブジェクト($order)の属性に値を代入する($order->user_id = Auth::user()->id;)
それでは下記の商品を保存してみましょう!
スクリーンショット 2021-02-25 19.44.46.png

③-1. まず注文情報(ユーザID・注文日・注文番号)のオブジェクト生成します

  • 注文情報テーブル(t_ordersテーブル)のオブジェクト生成($order = new \App\Order;)
  • カラムは、「ユーザID」「注文日」「注文番号」の3つです
この時点でのオブジェクト変数`$order`のデバック内容
App\Order {#1177 ▼
  #table: "t_orders"
  +timestamps: false                        
  #fillable: array:3 [▼
    0 => "user_id"
    1 => "order_date"
    2 => "order_number"
  ]
~省略~
  #attributes: []     まだオブジェクトには何も代入されていない
~省略~
  ]
}

③-2. オブジェクトの属性(#attributes)に値を代入

  • 下記の実行
    • $order->user_id = Auth::user()->id;
    • $order->order_date = $now;
    • $order->order_number = rand();
属性代入後のオブジェクト変数`$order`のデバック内容
(抜粋)
#attributes: array:3 [▼
    "user_id" => 1
    "order_date" => Carbon\Carbon @1614254126 {#1182▼
~省略~
      date: 2021-02-25 19:50:36.828250 Asia/Tokyo (+09:00)
    }
    "order_number" => 187331179
  ]

オブジェクトの属性にそれぞれの値が代入されましたね!

③-3. saveメソッドで新しいレコードをDBに挿入する

//認証済みのユーザーのみDBへ保存可能
Auth::user()->orders()->save($order);

やっていることはコメントどおりなのですが、具体的に何をしているのか見てみましょう。
これはUserモデルとリレーションしているOrdersモデルの関係性を用いてDBにレコードの挿入をしています。
認証していないユーザにDBへのレコード保存をさせないためです。
※(今回は実装しませんでしたがPoliyを用いて認可機能も実装させるべきだと思います)
参考:よくわかる認証と認可

いま誰が注文しようとしているのか?誰に注文させてよいのか?
これをプログラムする必要があります。
この「注文」と「誰が注文したのか」を注文時に判別出来るように条件付をするのです。
その条件が
「いま認証されているユーザだけが注文できる」
となります。正確には
「いま認証されているユーザだけがt_ordersテーブルの上記の3つのカラムに値を保存できる」
とでもいうのでしょうか。

ここで出てくるのがリレーションです。
ここでリレーションしているモデルとは勿論、UserモデルとOrdersモデルです。

User.php
(抜粋)
public function orders()
    {
        return $this->hasMany('App\Order');
    }

・一人のユーザーが複数の注文を所持するのでhasMany(わたしもつたくさん、の意味)を用い、メソッド名は複数形のordes
・反対に一つの注文は一人のユーザからしか生じないのでbelongsTo(〜に属する、の意味)を用いメソッド名は単数形のuser

Order.php
(抜粋)
public function user()
    {
        return $this->belongsTo('App\User');
    }

リレーションさせることで
「認証済みのユーザ」と「注文情報」を紐付け出来るようになります。
これで認証済みユーザのみがDBに保存できるプログラムを書く準備ができました。
それではコードを再び見ていきましょう。
Auth::user()->orders()->save($order);

これまでのリレーション先の値を取得したかった場合の記述方法
上記のUserモデルとリレーションしているOrdersモデルの関係の場合
リレーション先の値を取得する場合の記述は下記のようになります。
そして返ってくるのはCollectionオブジェクトです。各モデルのプロパティを取得したい場合
上記でもありましたが、このオブジェクト変数はCollectionなので繰り返し処理すれば良いです。

認証済みユーザの全Order取得
$user = \App\User::find(Auth::user()->id);
$orders = $user->orders;

foreach($orders as $order) {
    $userOrder = $oreder->order_date;
}
$ordersのデバック内容
Illuminate\Database\Eloquent\Collection {#1232 ▼
  #items: array:30 [▼
    0 => App\Order {#1233 ▶}
    1 => App\Order {#1234 ▶}
~省略~

$userOrderのデバック内容
"2021-02-25 21:28:56"

ここでコードを比較してみます
Auth::user()->orders()->save($order);

$user->orders;
注目してほしいのはordersの後ですね。
上記では()がない形でした。この場合、動的プロパティ(値)というものを呼び出しています。
上記のような複数のModelオブジェクトが格納されいるCollectionオブジェクトや
単体としてのModelオブジェクトが返ってきますね。

ではリレーションメソッドであるorders()の場合なにが返ってくるのでしょう。
return $this->hasMany('App\Order');がメソッドの内容ですが、この内容だと
下記のような「HasManyオブジェクト」というものを返します。

Illuminate\Database\Eloquent\Relations\HasMany {#1197 ▼
  #foreignKey: "t_orders.user_id"
  #localKey: "id"
  #query: Illuminate\Database\Eloquent\Builder {#1194 ▼
    #query: Illuminate\Database\Query\Builder {#1195 ▶}
    #model: App\Order {#1193 ▶}
    #eagerLoad: []
    #localMacros: []
    #onDelete: null
    #passthru: array:17 [▶]
    #scopes: []
    #removedScopes: []
  }
  #parent: App\User {#1200 ▶}
  #related: App\Order {#1193 ▶}
}

ここで重要なのはHasManyオブジェクトなどのEloquentリレーションオブジェクトは
クエリビルダとしても動作する
ということです。おそらく、この記述の部分で解るようにBuilderクラスをラップしているからだと思います。
#query: Illuminate\Database\Eloquent\Builder {#1194 ▼
#query: Illuminate\Database\Query\Builder {#1195 ▶

再びコードです。
Auth::user()->orders()->save($order);

クエリビルダは Laravel で SQL の記法を書きやすくしたものです。
->save($order)といった記法をメソッドチェーン的に繋げることが可能になり、これによってDBへのレコードの挿入が可能になります。

saveメソッドの第一引数には保存先を指定しないといけません。
ここが抜けると何処に保存するのかわからなくなりますし、引数が足りませんと怒られます。

スクリーンショット 2021-02-25 22.12.28.png

ちなみに、リレーションオブジェクトからは勿論プロパティの取得は出来ないです。
#attributesになんの値も入っていないので加工しようが繰り返し処理しようがないものはないのです。
値を取得したければリレーションメソッドを使用せずに、動的プロパティを取得しましょう。

それではsaveメソッド実行後の$order内にある#attributesの変化を見てみましょう!

#attributes: array:4 [▼
    "user_id" => 1
    "order_date" => Carbon\Carbon @1614254126 {#1182 ▶}
    "order_number" => 187331179
    "id" => 27
  ]

$orderの各属性に代入した値(上記の$orderのデバック内容参照)が代入されていますね。
それに加え
"id" => 27
という連想配列もあります。
これはt_ordersテーブルの主キーです。オートインクリメントなので注文数(処理数)に合わせてidも正比例して増加します。
ややこしいのですが、注文数=id番号数ではないのですが、大して重要ではないので理由は割愛します。

(ここらへんがちんぷんかんぷんな方は、マイグレーションファイルのコメントアウト部分や参考先のサイトを見てくださいませ。)

この値でt_ordersテーブルとt_order_detailsテーブルの指定したカラムを紐付けますが、解説は次でします!

これにて
Auth::user()->orders()->save($order);
「認証済みユーザのみがDBに保存できる」ようになりました!

ちなみにですがAuth::user()->orders->save($order);では勿論保存できません。
スクリーンショット 2021-02-26 23.54.10.png
動的プロパティの中身はモデルオブジェクトだったりするのですが、その中にはsaveメソッドはないんですね。
上記のhasManyオブジェクトには有るんですよね、saveメソッドが。
このオブジェクト内の、継承しているなにかのクラスがsaveメソッドを持っているのだと思いますが、はっきりはわかりません。
Builderクラスの中のどこかに継承されているのでしょうかね?

それでは続きの解説といきましょう!もう少しでおしまいです!
処理の流れの再確認です

t_order_detailsテーブル(注文詳細情報)への保存

2-1.t_ordersテーブルとt_order_detailsテーブルの指定したカラムを紐付けた値を取得する
2-2.保存したいデータに対応するモデルのオブジェクトを作成する
2-3.保存したい値をオブジェクトに格納する
2-4.注文詳細情報保存を注文数分繰り返す(foreach)
カラム
・ id
・ product_id
・ order_id(外部キー)
・ shipment_status_id
・ order_quantity
・ shipment_date

2-1.t_ordersテーブルとt_order_detailsテーブルのレコードを紐付けるための値を取得する
これは上記の
t_ordersテーブルの主キーであるidだけを取得する必要があります

該当するデータだけ取得したい場合where()で指定することで可能です。
第一引数には条件付けを行うカラム、第二引数には条件となる値を渡します。

t_ordersテーブルのカラム「order_number」が「$order->order_number」という
レコードを取得したい場合、
つまり今t_ordersテーブルに保存したorder_numberに該当するレコードを取得したい場合、
下記のような記述となります。

//Orderテーブルの カラム「order_number」が「$order->order_number」の最新のレコードを一つ取得
$savedOrder = Order::where('order_number', $order->order_number)->latest()->first();

//上記Collectionから id の値だけを取得した配列に変換
$savedOrderId = $savedOrder->pluck('id')->toArray();

$savedOrderのデバック内容を見てみましょう!

(抜粋)
#attributes: array:4 [▼
    "user_id" => 1
    "order_date" => Carbon\Carbon @1614254126 {#1182 ▶}
    "order_number" => 187331179
    "id" => 27
  ]

先程保存されたレコードが取得できました!

②このコードは上記のレコードから、主keyであるidだけを抽出し配列に変換しています。
plunkメソッドでkeyidの値を抽出しtoArrayメソッドで配列化しています。
配列化前はCollectionオブジェクトです。このままでは配列の値を$savedOrderId[0]の様な記述で取り出せません。
$savedOrderId[0]という形で配列の値を取得できるようにするためにtoArrayメソッドで配列化します。

$savedOrder->pluck('id')の配列化前後のデバック内容
(配列化前)
Illuminate\Support\Collection {#1194 ▼
  #items: array:1 [▼
    0 => 27
  ]
}

(配列化後)
array:1 [
  0 => 27        
]

このidは先程も言ったように、
t_ordersテーブルとt_order_detailsテーブルの指定したカラムを紐付けた値
となります。

続きの処理を見ていきましょう!

//注文詳細情報保存を注文数分繰り返す 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 = 3;
    $orderDetail->order_quantity = $data['session_quantity'];
    $orderDetail->shipment_date = $now;
    Auth::user()->orderDetails()->save($orderDetail);
    }

一度の購入で複数の商品が有った場合、商品を個別にDBに保存する必要があるので繰り返し処理をします。
indexメソッドでの処理方法で途中まで同じです。必要な値を各々に代入しています。
保存処理も先程説明した部分と同じになります。

複数のテーブルのカラムを紐付ける:外部キー制約

例として、一度の注文で作成される各テーブルのレコードを見てみましょう。
下記のようなテーブル構造に値が保存されます。

・t_ordersテーブル = 「注文情報」

id(主キー) user_id order_date order_number
1 2 2021-02-25 20:55:26 187331179

・t_order_detailsテーブル = 「注文詳細情報」

id product_id order_id(外部キー) shipment_status_id order_quantity shipment_date
1 1 1 3 4 2021-02-26 14:05:12
2 2 1 3 10 2021-02-26 14:05:12
3 4 1 3 3 2021-02-26 14:05:12
4 6 1 3 6 2021-02-26 14:05:12

ここで注目してほしいのは、「注文情報」のidカラムとorder_idです。
ここの値は連動していて両方とも「1」になっています。
両テーブルでこの値は同一のものとなります。これは外部キー制約という機能で成立しています。

このテーブルは有るユーザの買い物と、その買い物の詳細情報で分割されています。
「誰がいつ買い物をしたのか?」              注文情報
「どんな買い物をして、現在の状態(発送の有無)は?」   注文詳細情報

注文詳細情報テーブルでは「誰が」「いつ買ったのか?」、がわかりません。
注文情報テーブルでは「誰が」「いつ」この買物をしたのか、がわかりません。
なので両テーブルで指定されたカラム(id=order_id)は双方のテーブルからお互いを参照するためのカラムとなります。

注文詳細情報テーブルから「誰が」「いつ買ったのか?」を知りたければ、order_idからidを辿れば知ることが出来ます。
その逆に、注文情報テーブルでは「誰が」「いつ」この買物をしたのか知りたければidからorder_idを辿れば知ることが出来ます。

両テーブルで指定されたカラム(id=order_id)によって両テーブルの関連性が保たれています。
もし、指定されたカラムの値が片方だけ変化してしまった場合、もう片方もそれに連動して変化しなくてはなりません。
(更新 or 削除 or 変更不可)
片方だけ変化してしまった場合、テーブル間のデータの整合性が失われてしまいます。
それをDBそのものに保証させるのが外部キー制約で、この外部キー制約によってDBのデータの整合性は保たれます。


外部キー制約とは
以下のマイグレーションファイルはt_order_detailsテーブル構造を定義しています。
このマイグレーションファイルをmagraiteすることでt_order_detailsテーブルが作成されます。

スクリーンショット 2021-02-27 20.56.42.png


外部キー制約とは

外部キー制約は下記のような挙動を定義することが出来ます。
①親テーブルのレコードに対して、削除または更新を行うと、子テーブル内で同じ値を持つカラムのデータに対して削除または更新を行う
②=親テーブルのレコードに対し、削除または更新を行うとエラーとなる

・他のテーブルのデータに参照(依存)するようにカラムにつける制約のこと。
  ・参照されるのが親テーブル参照するのが子テーブルと呼ぶ(親テーブル = t_ordersテーブル)
  ・外部キーは主キーと結びつく
・主キーと外部キーはRDBにとって、それぞれのテーブルを関連付けるために使用するとても大切な機能。
  ・RDB = リレーショナル・データベース
  ・主キーと外部キーを使った制約で利用した場合、下記の制限が入る
     ・1. 存在しない値を外部キーとして登録することはできない
     ・2. 子テーブルの外部キーに値が登録されている親テーブルのレコードは削除できない
・各テーブルの主キーと外部キー、それぞれの値が連動することでテーブル間の値の整合性が保たれる。

 
・それでは上記の知識を踏まえてこのコードの解説です。

①$table->foreign('order_id')②->references('id')->on('t_orders')③->onDelete('cascade');



①. $table->foreign('order_id')
➡ このテーブル( t_orderDetail テーブル)の order_id カラムを外部キーに設定
②. ->references('id')->on('t_orders')
➡ t_orders テーブル(親テーボォ)の id を参照します(に依存します)。
➡ t_orders テーブル(親)の主キー id カラムと t_orderDetail テーブル(子)の外部キー order_id カラムが紐づく
③. ->onDelete('cascade');
➡ 親テーブルのレコードに対して削除を行うと、子テーブル内で同じ値を持つカラムのデータに対し削除を行う


これで2つのカラムが紐づき、テーブル間でのデータの整合性が保たれました。
どちらかの値が削除された場合、両方削除され片方だけ残る様な状況が防げますね。
そうならないような保証をDBにさせるのが外部キー制約の役割です。
制約の仕方何種類かありますが、ここでは紹介しないので興味あれば調べてみて下さい。

それでは最後の処理です。

//session削除
$request->session()->forget('cartData');
return view('products/purchase_completed', compact('order'));

注文内容をDBに保存したら、商品情報を保存しているセッション情報は不要になります!
削除しましょう!

そして最後に購入完了画面を表示させます!お客様に、問い合わせ用の注文番号を表示させるために
viewにオブジェクト変数orderを渡します。

<p class="h1">注文番号:{{ $order->order_number }}</p>





これにて終了です!最後まで読んで下さった方、お疲れさまです!長くてすみません(´;ω;`)有難うございました!!
※訂正部分などありましたら教えていただくと泣いて喜びます。むしろお願いしますm(_ _)m
スクリーンショット 2021-02-28 13.57.55.png

参考にさせていただいたサイト

33
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
31