スクリーンショット_2020-01-31_18

Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る⑤

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

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

■最初の記事↓
https://note.com/mukae9/n/n12cc13fd4f90

■2番目の記事↓
https://note.com/mukae9/n/naf7dff31b4db

■3番目の記事↓
https://note.com/mukae9/n/n2ed6be437f0d

■4番目の記事↓
https://note.com/mukae9/n/n103587d08ef2

です!

今回の記事では

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

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

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

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

1、ヘッダーを編集する



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

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

<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">

   <!-- CSRF Token -->
   <meta name="csrf-token" content="{{ csrf_token() }}">

   <title>{{ config('app.name', 'Laravel') }}</title>

   <!-- Scripts -->
   <script src="{{ asset('js/app.js') }}" defer></script>

   <!-- Fonts -->
   <link rel="dns-prefetch" href="//fonts.gstatic.com">
   <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">

   <!-- Styles -->
   <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>

<body>
   <div id="app">
       <nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
           <div class="container">
               <a class="navbar-brand" href="{{ url('/') }}">
                   {{ config('app.name', 'Laravel') }}
               </a>
               <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
                   <span class="navbar-toggler-icon"></span>
               </button>

               <div class="collapse navbar-collapse" id="navbarSupportedContent">
                   <!-- Left Side Of Navbar -->
                   <ul class="navbar-nav mr-auto">

                   </ul>

                   <!-- Right Side Of Navbar -->
                   <ul class="navbar-nav ml-auto">
                       <!-- Authentication Links -->
                       @guest
                           <li class="nav-item">
                               <a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
                           </li>
                           @if (Route::has('register'))
                               <li class="nav-item">
                                   <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
                               </li>
                           @endif
                       @else
                           <li class="nav-item dropdown">
                               <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
                                   {{ Auth::user()->name }} <span class="caret"></span>
                               </a>

                               <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                                   <a class="dropdown-item" href="{{ route('logout') }}"
                                      onclick="event.preventDefault();
                                                    document.getElementById('logout-form').submit();">
                                       {{ __('Logout') }}
                                   </a>

                                   <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                                       @csrf
                                   </form>
                               </div>
                           </li>
                       @endguest
                   </ul>
               </div>
           </div>
       </nav>

       <main class="py-4">
           @yield('content')
       </main>
   </div>

</body>
</html>

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


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

まず最初に飛び込んでくるこちら。
<a class="navbar-brand" href="{{ url('/') }}">
  {{ config('app.name', 'Laravel')
</a>

{{ }}とかがあると困惑しますし他にもちょこちょこありますが、
だいたいはconfigフォルダのapp.phpを見に行っているだけです。
さらにapp.phpは.envファイルを見に行ってるので
.envに書かれた情報が表示されています。

ちなみに
{{ config('app.name', 'Laravel')

のLaravelはapp.nameに値がない時に表示される文字列なのでここ変えても意味ないです。

せっかくなので.envの
一番最初にある
APP_NAME=を
APP_NAME=LaraShopに変えておきましょう。

スクリーンショット 2020-01-27 18.52.03

変わりましたね。
ちなみにページタイトルも連動して変わっています!
とりあえず仕組みとして理解していて貰えばと思います。

次に気になるのが、@guestです。
bladeファイルの書き方ですが、
とても便利で、

@guest
 //ログインしていないユーザーに表示
@else
 //ログインしているユーザーに表示
@endguest

ということが簡単に出来ます。

また逆に
@auth
  //ログインしているユーザーに表示
@endauth
ということも可能です。

@if (Route::has('register'))
@endif

はもうそのままですがregisterというrouteが定義されていれば処理
ってことになります。
ちなみにregisterのルートなんて作った記憶はないと思いますが、
最初の記事で認証機能を作った際に、
routes/web.phpのAuth::routes();
が自動で追加されていまして、この部分で裏で定義済みです。
registerだけでなくloginルートとかもこの時に全て作られているから普通に動いてるわけです。(中身はvenderフォルダにあります!)

ちょっとapp.blade.phpの中身がわかってきたところで、
「カートを見る」を追加したいと思います。

ドロップダウンのメニューにも「カートを見る」を追加します。

せっかくなのでログイン状態の名前が表示される横に
カートマークも置いて見ることにしましょう。

2、画像を表示させる


編集後のapp.blade.phpを見てみましょう。
(どさくさに色も変えてみました。分かりやすくするためにCSS直接打ってるので気になる方はscssの方に書いてあげてください。)

<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">

   <!-- CSRF Token -->
   <meta name="csrf-token" content="{{ csrf_token() }}">

   <title>{{ config('app.name', 'Laravel') }}</title>

   <!-- Scripts -->
   <script src="{{ asset('js/app.js') }}" defer></script>

   <!-- Fonts -->
   <link rel="dns-prefetch" href="//fonts.gstatic.com">
   <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">

   <!-- Styles -->
   <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
   <div id="app">
       <nav class="navbar navbar-expand-md navbar-light  shadow-sm" style="background-color:#0092b3; color:#fefefe;">
           <div class="container">
               <a class="navbar-brand" style="color:#fefefe; font-size:1.4em" href="{{ url('/') }}" >
                   {{ config('app.name', 'Laravel') }}
               </a>
               <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
                   <span class="navbar-toggler-icon"></span>
               </button>

               <div class="collapse navbar-collapse" id="navbarSupportedContent">
                   <!-- Left Side Of Navbar -->
                   <ul class="navbar-nav mr-auto">

                   </ul>

                   <!-- Right Side Of Navbar -->
                   <ul class="navbar-nav ml-auto" >
                       <!-- Authentication Links -->
                       @guest
                           <li class="nav-item">
                               <a class="nav-link" style="color:#fefefe;"  href="{{ route('login') }}">{{ __('ログイン') }}</a>
                           </li>
                           @if (Route::has('register'))
                               <li class="nav-item">
                                   <a class="nav-link" style="color:#fefefe;"  href="{{ route('register') }}">{{ __('会員登録') }}</a>
                               </li>
                           @endif
                       @else
                           <li class="nav-item dropdown">
                               <a id="navbarDropdown" style="color:#fefefe;" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
                                   {{ Auth::user()->name }} <span class="caret"></span>
                               </a>

                               <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                                   <a class="dropdown-item" href="{{ route('logout') }}"
                                   onclick="event.preventDefault();
                                                   document.getElementById('logout-form').submit();">
                                       {{ __('ログアウト') }}
                                   </a>

                                   <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                                       @csrf
                                   </form>

                                   {{-- 追加 --}}
                                   <a class="dropdown-item" href="{{ url('/mycart') }}">
                                       カートを見る
                                   </a>
                               </div>
                           </li>
                           {{-- 追加 --}}
                           <a href="{{ url('/mycart') }}" >
                               <img src="{{ asset('image/cart.png') }}" class="cart" >
                           </a>
                       @endguest


                   </ul>
               </div>
           </div>
       </nav>

       <main class="py-4">
           @yield('content')
       </main>
   </div>
</body>
</html>

もちろん画像がないので画像をpublic/imageフォルダを作って保存しておきます。
こういうことですね↓

スクリーンショット 2020-01-27 19.32.46


画像は
https://icooon-mono.com/00121-%e3%82%ab%e3%83%bc%e3%83%88%e3%81%ae%e3%82%a2%e3%82%a4%e3%82%b3%e3%83%b3%e7%b4%a0%e6%9d%90/
などで良いのではないでしょうか。

ちなみにですがこういった個人情報ではない画像はここでいいですが、
例えばSNSサイトのプロフィール写真などなどをここに入れてしまうと外部から覗くことが出来てしまうのでNGです。

この記事ではそういった個人情報が絡む画像は扱わないので詳しい説明は避けますが、
そういう画像を使うことが考えられる場合はこの記事など参考になります。
https://qiita.com/u-dai/items/8a904cc7fd2795c0e70d

さて、現在カートページを見ると(ログインしてたら)

スクリーンショット 2020-01-27 19.45.57

こんな感じになってると思います。

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

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

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

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

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

スクリーンショット 2020-01-31 12.46.34

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

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


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

スクリーンショット 2020-01-31 12.52.31

はい。
エラーですね。
一気にテンションが下がりました。

ただよく見るともう具体的にエラー内容と対策を書いてくれています。

$message is undefined
Make the variable optional in the blade template. Replace {{ $message }} with {{ $message ?? '' }}

$messageが定義されてない。
{{ $message ?? '' }}
こっち使えば?

って言ってくれてますね。

mycart.blade.phpでは{{ $message }}が記述されていますが、
この$messageに値がないケースで発生するエラーです。

カートに追加するの場合(POST送信のルート)で
「追加済みです」
「追加しました」
を表示するためのものですね。

なので削除するわけにはいかないので
{{ $message ?? '' }}と置き換えてあげます。

すると値がなければ空表示という$messageのデフォルトを指定することになります。

mycart.blade.phpで{{ $message ?? '' }}に置き換えて表示してみましょう。

スクリーンショット 2020-01-31 13.07.55

その後リロードしたら、、、。
表示されましたね!

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

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

ShopControllerを見て行きましょう。

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

  public function myCart()
  {
      $my_carts = Cart::all();
      return view('my_carts',$my_carts);
  }

Cart::all()はやばいですね。
仮で課題作っているやつですので明らかにおかしいです。
このままだとAmazonの二の舞になりそうです。

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


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

 public function myCart()
 {
    $user_id = Auth::id();
    $my_carts = Cart::where('user_id',$user_id)->get();
    return view('mycart',compact('my_carts'));
 }


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

ただそろそろソワソワしてきた人もいると思いますが、
気付けばMVCモデルの基本を無視して、Controllerにモデル側の機能を書いてしまってますね。
あるあるです。

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

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

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

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

<?php

namespace App\Models;

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

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

正直Cart::でも動くのですが、Cart::って自分のこと(Cartクラス)ですので
「マイちゃんの〜」って自分自身のことを名前で呼ぶ感じの辛さがあるので
$this->にして「私の」って感じにしてます。
※マイって名前の人見てたらすいません。あなたの事ではないです。同郷のアイツです。

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

   public function myCart(Cart $cart)
   {
       $my_carts = $cart->showCart();
       return view('mycart',compact('my_carts'));
   }

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

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

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

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

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

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

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

なんだか難しそうですね。
SQLでjoinすればいいじゃん!って人はそれも正解です。
というかそれでいいです。

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

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

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

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

この時Laravel側で勝手にcartsテーブルにはstock_idという値があると仮定。
stocksテーブルのidと紐付ければいいと判断します。

相変わらず過保護です。

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

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

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

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


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

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

ShopController.php

    public function addMycart(Request $request,Cart $cart)
   {

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

       //追加後の情報を取得
       $my_carts = $cart->showCart();

       return view('mycart',compact('my_carts' , 'message'));

   }

Cart.php

    public function addCart($stock_id)
   {
       $user_id = Auth::id(); 
       $cart_add_info = Cart::firstOrCreate(['stock_id' => $stock_id,'user_id' => $user_id]);

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

       return $message;
   }


4、カート内を表示する

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

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

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

    @foreach($my_carts as $my_cart)
       <div class="mycart_box">
           {{$my_cart->stock->name}} <br>                                
           {{ number_format($my_cart->stock->fee)}}円 <br>
               <img src="/image/{{$my_cart->stock->imgpath}}" alt="" class="incart" >
               <br>
               
               <form action="/cartdelete" method="post">
                   @csrf
                   <input type="hidden" name="stock_id" value="{{ $my_cart->stock->id }}">
                   <input type="submit" value="カートから削除する">
               </form>
       </div>
   @endforeach

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

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

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

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

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

スクリーンショット 2020-01-31 18.27.56

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


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

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


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


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

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

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

・カート内削除の実装
・合計金額の表示
・購入ボタンの追加
・購入メールの送信
・微調整

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

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

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

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