見出し画像

Laravel10(PHP8.2)+LaravelSail+MySQL8+phpMyAminで簡易的なECサイトを作るチュートリアル⑤

お疲れ様です。
向江です。

ECサイトの制作もなんとなく終わりが見え出している段階になりました。

今回の記事では

■一覧ページに画像を表示

■カート機能を完成(stockIdを使って商品内容や画像を表示)

を目標にしたいと思います。
まずそもそも「カートを見る」とかのリンクがないですね。
カートに追加しない限りカート内を見れない恐ろしいUIになっています。

ヘッダー部分に「カートを見る」を実装したいと思います。

1、ヘッダーを編集する



ヘッダー部分は
resouces/views/layoutの
navigation.blade.phpに書かれています。

とりあえず見てみましょう↓

<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
    <!-- Primary Navigation Menu -->
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div class="flex justify-between h-16">
            <div class="flex">
                <!-- Logo -->
                <div class="shrink-0 flex items-center">
                    <a href="{{ route('dashboard') }}">
                        <x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
                    </a>
                </div>

                <!-- Navigation Links -->
                <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
                    <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
                        {{ __('Dashboard') }}
                    </x-nav-link>
                </div>
            </div>

            <!-- Settings Dropdown -->
            <div class="hidden sm:flex sm:items-center sm:ml-6">
                <x-dropdown align="right" width="48">
                    <x-slot name="trigger">
                        <button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
                            <div>{{ Auth::user()->name }}</div>

                            <div class="ml-1">
                                <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                                    <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
                                </svg>
                            </div>
                        </button>
                    </x-slot>

                    <x-slot name="content">
                        <x-dropdown-link :href="route('profile.edit')">
                            {{ __('Profile') }}
                        </x-dropdown-link>

                        <!-- Authentication -->
                        <form method="POST" action="{{ route('logout') }}">
                            @csrf

                            <x-dropdown-link :href="route('logout')"
                                    onclick="event.preventDefault();
                                                this.closest('form').submit();">
                                {{ __('Log Out') }}
                            </x-dropdown-link>
                        </form>
                    </x-slot>
                </x-dropdown>
            </div>

            <!-- Hamburger -->
            <div class="-mr-2 flex items-center sm:hidden">
                <button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
                    <svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
                        <path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
                        <path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
                    </svg>
                </button>
            </div>
        </div>
    </div>

    <!-- Responsive Navigation Menu -->
    <div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
        <div class="pt-2 pb-3 space-y-1">
            <x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
                {{ __('Dashboard') }}
            </x-responsive-nav-link>
        </div>

        <!-- Responsive Settings Options -->
        <div class="pt-4 pb-1 border-t border-gray-200">
            <div class="px-4">
                <div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
                <div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
            </div>

            <div class="mt-3 space-y-1">
                <x-responsive-nav-link :href="route('profile.edit')">
                    {{ __('Profile') }}
                </x-responsive-nav-link>

                <!-- Authentication -->
                <form method="POST" action="{{ route('logout') }}">
                    @csrf

                    <x-responsive-nav-link :href="route('logout')"
                            onclick="event.preventDefault();
                                        this.closest('form').submit();">
                        {{ __('Log Out') }}
                    </x-responsive-nav-link>
                </form>
            </div>
        </div>
    </div>
</nav>

うわぁ…ってなりますね。
意外と詰め込まれています。
ちなみにclass名がたくさんくっついていますが
これもtailwindです。


head内は別記事で説明するので
今回はbody内を見て行きます。

まず最初に飛び込んでくるこちら。

            <div class="flex">
                <!-- Logo -->
                <div class="shrink-0 flex items-center">
                    <a href="{{ route('dashboard') }}">
                        <x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
                    </a>
                </div>

                <!-- Navigation Links -->
                <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
                    <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
                        {{ __('Dashboard') }}
                    </x-nav-link>
                </div>
            </div>


route('dashboard')
{{ __('Dashboard') }}
:active="request()->routeIs('stock.index')

が気になりますね。

ちなみにこれはヘッダー左の方にある今はdashboardと書かれた部分を表示しているコードになります。

まずroute('')はroutes/web.phpで指定していた->name('stock.index')とかがついに活躍することになります。(コピペで気付いてない人は見てみましょう)

遷移先のURLを名前付けしているのでこうやって名前だけで呼べるんですね。コードもすっきりしますしURLを直接書かずに済みますし万が一URLが変わってもweb.php側だけを変えれば全ページに反映されます。

せっかくなのでこのdashboardへの遷移をMyCartへの遷移に変えてみましょうか。


{{ __('Dashboard') }}
続いてこちらですが、
これは多言語対応をするための書き方です。
今回は多言語対応をする予定はないので塗りつぶしてMyCartってしちゃいましょう。


:active="request()->routeIs('stock.index')
最後にこちらは、現在のURLがstock.indexだったらactiveにするってことです。
後で実際に見てみるとわかりやすいのでとりあえず変更しておきましょう。

最終的にこうなりました。

            <div class="flex">
                <!-- Logo -->
                <div class="shrink-0 flex items-center">
                    <a href="{{ route('stock.index') }}">
                        <x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
                    </a>
                </div>

                <!-- Navigation Links -->
                <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
                    <x-nav-link :href="route('stock.index')" :active="request()->routeIs('stock.index')">
                        MyCart
                    </x-nav-link>
                </div>
            </div>

画面も見てみましょう!



変わりましたね。

MyCartもクリックしてみましょう。


ちゃんと遷移してくれましたね。
しかもしっかり下線が、、、、!
:active="request()->routeIs('stock.index')
の力ですね。

こうなると商品一覧への導線も作りたくなりますね。
とっても簡単に作れちゃいます。

    <!-- Navigation Links -->
    <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
        <x-nav-link :href="route('stock.index')" :active="request()->routeIs('stock.index')">
            商品一覧
        </x-nav-link>
    </div>
    <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
        <x-nav-link :href="route('stock.myCart')" :active="request()->routeIs('stock.myCart')">
            MyCart
        </x-nav-link>
    </div>


なんかよりそれっぽくなってきましたね!!!

2、画像を表示させる

商品の画像も表示しちゃいましょう。
すでにstocks.blade.phpに画像表示の仕組みは実装されているので画像をpublic/image/に保存するだけで大丈夫です。

ちなみに画像の表示の部分は
<img src="/image/{{$stock->iagePath}}" alt="" class="incart" >
これですね。

ここまで記事を勧めてくれた方は何をやっているか分かるかと思います。
???ってなった方は前の記事とかで復習してくださいね。

画像はこちら使ってください!


public/imageにフォルダの中身を移せばOKです!

こういう状態ですね。

画像が表示されました!
もうこれはほぼ完成な匂いですね!!

せっかくなのでカートに入れるボタンも装飾してみましょう!
文字が多いと改行してるのが気になるので文字も小さくしてみます!


<x-app-layout>
    <div class="container-fluid">
        <div class="mx-auto" style="max-width:1200px">
            <h1 style="color:#555555; text-align:center; font-size:1.2em; padding:24px 0px; font-weight:bold;">商品一覧</h1>
            <div class="">
                <div class="grid grid-cols-4 gap-4 flex-wrap">
                    @foreach($stocks as $stock)
                        {{-- text-smを追加 --}}
                        <div class="mycart_box text-sm text-center rounded shadow-lg bg-white p-6">
                            {{$stock->name}} <br>
                            {{$stock->fee}}円<br>
                            <img src="/image/{{$stock->imagePath}}" alt="" class="incart" >
                            <br>
                            {{$stock->explain}} <br>

                           
                            <form action="addMyCart" method="post">
                                @csrf
                                <input type="hidden" name="stockId" value="{{ $stock->id }}">
                               {{-- 変更 --}}
                                <button class="bg-blue-400 hover:bg-blue-700 text-white font-bold py-2 px-4 m-2 rounded ">カートに入れる</button>
                            </form>
                        
                        </div>

                    @endforeach
              
                </div>
                <div class="text-center" style="width: 200px;margin: 20px auto;">
                    {{  $stocks->links()}}
                </div>
                
            </div>
        </div>
    </div>
</x-app-layout>

今回もtailwindを使ってクラス名を指定するだけで簡単にボタンを作って見たりしました!
HTML/CSSをガッツリ描かなくていいので助かりますね、、、!

最終的にこんな感じになったかと思います!

ホバーで色が濃ゆくなったりもします!

3、カート機能を完成させる


それじゃあログイン状態で左上のカートをクリックしましょう!

いよいよ今回の記事の本題となる
カート機能の完成を行いたいと思います。

すでにカートから情報は引き出せているので、
stockIdを使って
stocksテーブルから情報を引き出せば商品名、画像の表示など簡単にやっていけそうですね。

StockControllerを見て行きましょう。

現在myCart()メソッドはこうなっていますね。

    public function myCart()
    {
        $myCartStocks = UserStock::all();
        return view('myCart',compact('myCartStocks'));
        
    }

UserStock::all()はやばいですね。全ユーザーのカート情報が出ちゃいます。
仮で課題作っているやつですので明らかにおかしいです。
このままだとかつてのAmazon事故の二の舞になりそうです。

まずはここを普通に自分のカート内情報だけが表示されるように編集します。
ちょっと考えて自分で実装してみてください。

。。。

はい、できましたかね。
色々な書き方あると思いますが、こんなもんでしょうか。
(本当はUserモデルにリレーションを書いてwithでした方がいいかもですが、あくまでこの教材はLaravelのなんとなくをなんとなく知る教材なので、これでいきます。Laravelに慣れてきたらLaravel EagerLoadingとかで検索してみてください。)

 public function myCart()
 {
    $userId = Auth::id();
    $myCartStocks = UserStock::where('userId',$userId)->get();
    return view('myCart',compact('myCartStocks'));
 }


何回も登場しているAuth::id()で
ログイン中の人のIdを取得してそれを元にカートテーブルから情報を取得しました。

ただそろそろソワソワしてきた人もいると思いますが、
気付けばMVCモデルの基本を無視して、Controllerにモデル側の機能を書いてしまってますね。
あるあるです。
この書き方をしたままポートフォリオとか提出しちゃうと、、、「分かってないな、、、」って思われる確率高いです。

この自分のカートの表示をする処理をUserStockモデル内でメソッド化してしまいましょう。

App/Models/UserStock.phpを開きます。
以下のメソッドを追加しましょう。

  public function showMyCart()
 {
       $userId = Auth::id();
       return $this->where('userId',$userId)->get();
 }

Authがまだ使える状態じゃないと思うので、
ページの上部にuseを追加します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth; ←追加

内容はほぼほぼStockController.phpのmyCartメソッドに書いている分と一緒ですが、
UserStock::が$this->に変わってますね。

正直UserStock::でも動くのですが、UserStock::って自分のこと(UserStockクラス)ですので
「マイちゃんの〜」って自分自身のことを名前で呼ぶ感じの辛さがあるので
$this->にして「私の」って感じにしてます。

さて、このおかげで
StockController.phpの方はこれらの処理の記述がいらないので
メソッドを呼び出す分に変えてあげます。

   public function myCart(UserStock $userStock)
   {
       $myCartStocks = $userStock->showMyCart();
       return view('myCart',compact('myCartStocks'));
   }

なんか public function myCart(UserStock $userStock)っと
引数を持ちましたね。

メソッドインジェクションです。
俗に言うDI(依存性注入)の方法の一つですね。

名前かっこいいですが、でやってくれているのは、
$userStock = new UserStock();
という通常のインスタンス化の記述を不要にしてくれてるくらいです。

もっと正確に言うと、
StockControllerクラスの myCartメソッド内で
new UserStock();
しちゃうとStockControllerとCartクラスが嫌でも強い結びつきになってしまっていますのでそれを避けれます。
テストの時とかに重宝します。

新しい言葉が出すぎて???になると思うので、
とりあえずこの手法なら、
$userStock = new UserStock()を
引数でUserStock $userStockって指定するだけで書けるぞ〜
くらいでいいんじゃないでしょうか。
確かに奥は深いですが参考書とか難しく書き過ぎてやる気無くしますよね。

さて後は、
$userStock->showMyCart();
これでインスタンス化したUserStock.phpのshowMyCart()を発動。
つまりさっきUserStock.phpのshowMyCart()に記載した処理を実行してくれて、returnなので結果を$myCartStocksに代入してくれることになります。

ただこれだけでは、
結局UserStockテーブルの内容しか取得することができません。
本当に欲しい情報は
UserStockテーブルでuserIdが一致したレコード(これは取れている)
に記載してあるstockIdと一致するStocksテーブルの商品情報です。

なんだか難しそうですね。
SQLでjoinすればいいじゃん!って人はそれも正解です。
というかそれでいいです。負荷が重い処理の時などはもはやそれがベストです。

ただLaravelを使っている以上、もう少しシンプルな方法もあるので、
書いておきます。

App/Models/UserStock.phpを開いて以下を追加してください。

   public function stock()
   {
       return $this->belongsTo('\App\Models\Stock','stockId');
   }

はい、これだけでOKです。
$this->belongsTo('\App\Models\Stock','stockId');
この記述でusers_stocksテーブルはstocksテーブルに従属する関係であることを表します。リレーションですね。

この時Laravel側で勝手にusers_stocksテーブルにあるidと第二引数でした自分自身のid(ここでいうとusers_stocksテーブルのstockId)
stocksテーブルのidと紐付ければいいと判断して勝手に紐づく商品を出してくれます。

相変わらず過保護です。

もちろん
$this->belongsTo('\App\Models\Stock', 'stockId' , 'ownerId');
と第二・第三引数を利用して紐付けるカラムを変更することも可能です。

リレーション(関係性)なので従属されてる側のStockモデル側にもhasOneとか書かねばと思う方もいらっしゃると思いますが、

今回のケースではStockモデルからしたらusers_stocksテーブルの存在などどうでもいいので関係性を結ぶ必要がありません。
(カート側は商品情報表示のために商品側の情報が必要(従属)、
商品側はユーザーのカート情報は不要)

片思いのままにしておきましょう。

そして以前の記事ではこのまま利用していましたが、
パフォーマンスを考慮してEagerLoadingを利用したwithでの書き方を記載します。
難しいワードが出てきますが書き方はいたって簡単です。
showMyCartメソッドに以下のwithを追加してあげてください。

public function showMyCart()
 {
       $userId = Auth::id();
       return $this->where('userId',$userId)->with('stock')->get();
 }

これでOKです。
なんのこっちゃかもしれませんが、これをやらないで次に進むと
A、B、Cと商品があったとするとusers_stocksのA、B、Cの情報を取得(SQL1回目)してからAの詳細情報(SQL2回目)、Bの詳細情報(SQL3回目)、、、商品がある限り永遠に、、、。

という取り方になってしまいます。
処理も遅いですしDBのリクエスト数も多くなるので最悪止まります笑

ここでのこのwithを使うことで、users_stocksのA、B、Cの情報を取得するときについでに商品の詳細情報も取ってきてねってSQLになるのでえらい違いとなります。
もはやこの処理は必須ですね。

ついでに
StockControllerのaddMycartメソッドもController側にModel書くべき記載があるのでUserStock.phpに移植します。
やっていることは上記の説明と同じなので答えだけ記載します。

どういう流れになっているかの復習に使ってください。

StockController.php

public function addMycart(Request $request,UserStock $userStock)
   {

       //カートに追加の処理
       $stockId=$request->stockId;
       $message = $userStock->addCart($stockId);

       //追加後の情報を取得
       $myCartStocks = $userStock->showMyCart();

       return view('myCart',compact('myCartStocks' , 'message'));

   }

UserStock.php

public function addMyCart($stockId)
   {
       $userId = Auth::id(); 
       $cartAddInfo = $this->firstOrCreate(['stockId' => $stockId,'userId' => $userId]);

       if($cartAddInfo->wasRecentlyCreated){
           $message = 'カートに追加しました';
       }
       else{
           $message = 'カートに登録済みです';
       }

       return $message;
   }


4、カート内を表示する

ここまででControllerとModel側の処理が終了しました。

すでにmyCart.blade.phpに情報は送られています。

myCart.blade.phpの@foreach〜@endforeach内を以下のように書き換えます。
この後実装するカートの削除機能に必要な部分も追加しておきました。

    
<x-app-layout>
    <div class="container-fluid">
        <div class="mx-auto" style="max-width:1200px">
            <h1 style="color:#555555; text-align:center; font-size:1.2em; padding:24px 0px; font-weight:bold;">
            {{ Auth::user()->name }}さんのカートの中身</h1>
                <p class="text-center">{{ $message ?? '' }}</p><br>
                <div class="">
               {{-- 追加 --}}               
               @foreach($myCartStocks as $stock)
                <div class="text-center rounded shadow-lg bg-white p-6 m-4">
                {{$stock->stock->name}} <br>                                
                {{ number_format($stock->stock->fee)}}円 <br>
                    <div class="incart flex justify-center p-4 m-4">
                      <img src="/image/{{$stock->stock->imagePath}}" alt=""  width="600">
                    </div>
                    <form action="/deleteMyCartStock" method="post">
                        @csrf
                        <input type="hidden" name="stockId" value="{{ $stock->stock->id }}">
                        <input type="submit" value="カートから削除する">
                    </form>
                </div>
               @endforeach
              {{-- ここまで --}}
                </div>
            </div>
    </div>
</x-app-layout>

なんとなーくやっていることは分かるかもしれません。
今まで同じく$myCartStocksを@foreachで展開しつつ->stockを使うことで
先ほどリレーションをしたstocksテーブルの情報お取得することが出来ます。

なので
$stock->stock->feeならそのカートの商品の金額が表示。
$stock->stock->nameならそのカートの商品名が表示されます。

新しく追加した
<form action="/deleteMyCartStock" method="post">
@csrf
<input type="hidden" name="stockId" value="{{ $stock->stock->id }}">
<input type="submit" value="カートから削除する">
</form>

は見ての通り、/deleteMyCartStockにPOSTでstockIdを持っていってます。

とりあえずこの時点でカートを表示させてみましょう。

商品が表示されたでしょうか。


今の状態では「カートから削除する」をすると当然ルートすら定義されていないのでエラーです。

と言うことでここの実装を課題にして次回の記事に繋げたいと思います!


5、次回の記事に行く前の課題


①/deleteMyCartStockにPOSTでアクセスした場合にStockControllerのdeleteMyCartStockメソッドを発動させる。
②Eloquantを使ってカートテーブルから該当の商品かつログインユーザーのidと一致するレコードを削除する。
③削除後にカートを表示する。
$messageに「商品をカート内から削除しました」と表示させる。

ちょっと説明少なめにしてみました。
削除部分は今までに出てきてないですが、調べれば簡単に出てくると思います。
調べる力も養ってください!

なお次回の記事で一応終了予定です。

・カート内削除の実装
・微調整

をしたいと思っています。

それでは次回の記事でもお待ちしております〜!

https://note.com/mukae9/n/nec638c68c53a

この記事が気に入ったらサポートをしてみませんか?