【Laravel】第三回 自動販売機システムを作ろう! 〜購入画面の作成〜

Laravel
マイケル
マイケル
どうもみなさんこんにちは!
マイケルです!
エレキベア
エレキベア
クマ〜〜〜〜〜〜!!
マイケル
マイケル
今回も前回に続いて、
自動販売機システムを作っていきます!
マイケル
マイケル
とりあえずはこれで最終回の予定だ!
メインとなる購入画面を作っていくぞ!
エレキベア
エレキベア
いよいよ大詰めクマ〜〜〜!!
スポンサーリンク

参考書籍

マイケル
マイケル
基本的な実装方法などは、
前回に引き続きこちらを参考にして進めていきます。

エレキベア
エレキベア
やったるクマ〜〜〜〜!!

購入画面の仕様

マイケル
マイケル
さて、第一回で説明しましたが
購入画面の仕様は以下のようになっています!
Screenshot 2020 11 12 23 24 54
↑自動販売機:購入画面
Screenshot 2020 11 12 22 19 51
マイケル
マイケル
上部の入金エリアでお金を投入し、
下部の飲み物をクリックすると購入する

という、自動販売機のシミュレータになります!
エレキベア
エレキベア
まんま自販機クマね
マイケル
マイケル
メンテナンス画面作成の時にはjuiceテーブルのみ使用していましたが、
自動販売機ごとの金額管理を行う必要があるため、
新たにmachineテーブルも使用します。
Screenshot 2020 11 12 8 14 25
エレキベア
エレキベア
確かmachineテーブルとjuiceテーブルは
1:Nの関係だったクマね
マイケル
マイケル
よく覚えてたね!
自動販売機ごとに飲み物の種類や在庫を管理するから
下記の図のようになるよ!
Screenshot 2020 11 12 8 14 32
マイケル
マイケル
このような関係にすることで、
自動販売機ごとに分けて処理することができるんだ!
01 buy
↑自動販売機内の飲み物の購入ができる
エレキベア
エレキベア
なるほどクマね〜〜
やったるクマよ〜〜〜〜!!
マイケル
マイケル
それじゃ作っていこう!

購入画面の作成

マイケル
マイケル
今回作成するのは下記の部分!
新たに作るmachineテーブルのモデルと、
「/buy」で実行する画面・処理の実装だ!
JuiceApp mindmap
エレキベア
エレキベア
これで全体が完成するクマね
マイケル
マイケル
もう少しの辛抱だ!
今回もMVCの順で見ていこう!

Model

マイケル
マイケル
まずはModelの作成からだ!
juiceテーブルのモデルはもう作成しているから、
新たにmachineテーブルのモデルを作成しましょう!
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Machine extends Model
{
    protected $table = 'machine';
    protected $primaryKey = 'id';
    protected $guarded = array('id');

    // 関連するjuceテーブルデータを取得
    public function juices()
    {
        return $this->hasMany('App\Juice', 'm_id');
    }
}
↑machineテーブルのModelクラス
エレキベア
エレキベア
変数の部分は前も出てきたクマが、
新たに作っている juicesメソッドは何クマ??
マイケル
マイケル
これは、machineテーブルに紐づくjuiceテーブルのレコードを取得するために
hasMany()メソッドを定義しているんだ!
マイケル
マイケル
これを定義して、View側で「juices」を指定することで、
machineテーブルの「id」と
juiceテーブルの「m_id(第2引数)」に紐づくレコード
を全て取得できるんだよ。
エレキベア
エレキベア
それは便利クマね・・・。
自動販売機のレコードに紐づく飲み物一覧を取得するには持ってこいクマ
マイケル
マイケル
そういうことだね!
表示の方法はこの後のViewの実装でみていこう!

View

マイケル
マイケル
購入画面を新たに作成するため、
下記Viewクラスを作成しましょう!
■ juiceapp.blade.php
格納先:app/resources/views/layouts
<html>
<head>
    <title>***ENEGY SOUL***</title>
    <style>
        body { font-family:Arial; font-size:16pt; color:#555555; background-color:#f5f5f5; margin:5px;}
        hr { margin:5px 20px; border-top:1px dashed #555555; }
        .title-area { position:relative; background-color:#333333; height:100px; width:100%; }
        .title-area span { position:absolute; top:10px; left:15px; color:#eeeeee; font-size:45pt; text-align:left; letter-spacing:-4pt; }
        .message-area { position:relative; height:50px; width:100%; font-size:13pt; overflow-y: scroll; }
        .message-area .msg { position:absolute; left:15px; }
        .message-area .err_msg { position:absolute; left:15px; color:red; }
        .footer { text-align:left; font-size:10pt; margin:10px; border-top:solid 1px #ccc; color:#ccc; }
        .juice-table th { background-color:#999; color:#fff; padding:5px 10px; }
        .juice-table td { vertical-align:top; border: solid 1px #aaa; color:#999; padding:5px 5px; }
        .juice-table .ju_image { width:70px; }
        .juice-table .buttun-area { background-color:none; border:none; display:table-cell; vertical-align:middle; }
        .juice-table .ins_button { background-color:#333333; height:45px; width:80px; border-radius:7%; color:#eee; font-size:15px; cursor:pointer; }
        .juice-table .upd_button { background-color:#0066FF; height:45px; width:80px; border-radius:7%; color:#eee; font-size:15px; cursor:pointer; }
        .juice-table .del_button { background-color:#FF3333; height:45px; width:80px; border-radius:7%; color:#eee; font-size:15px; cursor:pointer; }
        .image-table td { border:none; }
        .juice-table-title { font-size:18px; margin:15px 0px 0px 0px; }

        /* ↓以下のCSS定義を追加 */
        .money-area { height:70px; }
        .money-area .money-text-area { vertical-align:center; text-align:right; }
        .money-area .money-text { background-color:#333333; color:#eee; height:30px; width:120px; padding: 5px; font-size:20px; }
        .money-area .money-10 { background-color:#d2b48c; border-radius:50%; width:50px; height:50px; margin: 0px 5px 5px 5px; font-size: 18px; cursor:pointer; }
        .money-area .money-100 { background-color:#dcdcdc; border-radius:50%; width:50px; height:50px; margin: 0px 5px 5px 5px; font-size: 18px; cursor:pointer; }
        .money-area .money-500 { background-color:#f5f5dc; border-radius:50%; width:50px; height:50px; margin: 0px 5px 5px 5px;  font-size: 18px; cursor:pointer; }
        .money-area .money-1000 { background-color:#fff8dc; border-radius:5%; width:80px; height:45px; margin: 0px 5px 5px 5px;  font-size: 20px; cursor:pointer; }
        .money-area .money-oturi { background-color:#eee; border-radius:5%; width:42px; height:42px; font-size:15px; text-align:center; cursor:pointer; }
        .juice-container { display:flex; justify-content:flex-start; flex-wrap:wrap; width:100%; list-style:none; margin:0; padding:0; }
        .juice-container li { display:flex; flex-flow:column; width:300px; border:1px solid #ccc }
        .juice-container .ju-image { width:300px; height:300px; }
        .juice-container .juice-name { width:300px; text-align:center; }
        .juice-container .cold-msg { background-color:blue; color:#eee; width:300px; text-align:center; }
        .juice-container .juice-price { background-color:#333333; color:#eee; width:300px; text-align:right; }
        .juice-container .push-button-area { width:300px; text-align:center; margin: 10px; }
        .juice-container .push-button { background-color:#666666; color:#eee; border-radius:100vh; width:200px; font-size:30px; cursor:pointer; }
        .juice-container .push-button-disabled { background-color:#ccc; color:#eee; border-radius:100vh; width:200px; font-size:30px; }
        .juice-container .juice-stock { width:300px; text-align:right; }
    </style>
</head>
<body>
    <div class="title-area">
        <span>@yield('title')</span>
    </div>
    <div class="message-area">
        @yield('message')
    </div>
    <hr size="1">
    <div class="content">
        @yield('content')
    </div>
    <div class="footer">
        @yield('footer')
    </div>
</body>
</html>
■ index.blade.php
格納先:app/resources/views/buy
@extends('layouts.juiceapp')

@section('title', 'ENEGY SOUL*')

@section('message')
    @if (count($errors) > 0)
        <span class="err_msg">
            @foreach ($errors->all() as $error)
                <div>・{{$error}}</div>
            @endforeach
        </span>
    @else
        <span class="msg">
            @if (isset($machine))
                <div>・普通の自販機シミュレータでござる。</div>
            @else
                <div>・対象の自動販売機が無いでござる。</div>
            @endif
        </span>
        @endif
@endsection

@section('content')
    @if (isset($machine))
    <table class="money-area">
        <tr>
            <form action="/buy/money" id="moneyform" method="post">
                @csrf
                <input type="hidden" name="id" value="{{$machine->id}}">
                <td class="money-text-area"><div class="money-text">¥{{$machine->money}}</div></td>
                <td><input class="money-oturi" type="submit" name="oturi" value="返却"></td>
                <td><input class="money-10" type="submit" name="10button" value="10"></td>
                <td><input class="money-100" type="submit" name="100button" value="100"></td>
                <td><input class="money-500" type="submit" name="500button" value="500"></td>
                <td><input class="money-1000" type="submit" name="1000button" value="1000"></td>
            </form>
        </tr>
    </table>
<br>
    @if ($machine->juices != null)
        <ul class="juice-container">
            @foreach ($machine->juices as $juice)
                <li>
                    <form action="/buy" id="buyform_{{$juice->id}}" method="post">
                        @csrf
                        <input type="hidden" name="id" value="{{$juice->id}}">
                        <input type="hidden" name="m_id" value="{{$juice->m_id}}">
                        <input type="hidden" name="price" value="{{$juice->price}}">
                        <input type="hidden" name="money" value="{{$machine->money}}">
                        <img class="ju-image" src="data:image/jpeg;base64,{{$juice->ju_image}}" alt="image">
                        <div class="juice-name">{{$juice->name}}</div>
                        <div class="cold-msg">つめた〜い</div>
                        <div class="juice-price">{{$juice->price}}円</div>
                        <div class="push-button-area">
                        @if (0 < $juice->stock)
                            <input class="push-button" type="submit" value="PUSH">
                        @else
                            <input class="push-button-disabled" type="submit" value="SOLD" disabled>
                        @endif
                        </div>
                        <div class="juice-stock">在庫:{{$juice->stock}}個</div>
                    </form>
                </li>
            @endforeach
        </table>
    @endif
    @endif
@endsection

@section('footer')
    copyright 2020 都会のエレキベア.
@endsection
マイケル
マイケル
「juiceapp.blade.php」は前回作成しているからCSSの追加のみ!
メインとなる購入画面は「buy」フォルダの下に作成しましょう!
マイケル
マイケル
それでは各処理部分を見ていきます!
エレキベア
エレキベア
わくわくクマ〜〜〜〜〜

・入金処理の記述
マイケル
マイケル
画面上部にある入金エリアはこんな感じだ!
Screenshot 2020 11 12 23 24 54


    <table class="money-area">
        <tr>
            <form action="/buy/money" id="moneyform" method="post">
                @csrf
                <input type="hidden" name="id" value="{{$machine->id}}">
                <td class="money-text-area"><div class="money-text">¥{{$machine->money}}</div></td>
                <td><input class="money-oturi" type="submit" name="oturi" value="返却"></td>
                <td><input class="money-10" type="submit" name="10button" value="10"></td>
                <td><input class="money-100" type="submit" name="100button" value="100"></td>
                <td><input class="money-500" type="submit" name="500button" value="500"></td>
                <td><input class="money-1000" type="submit" name="1000button" value="1000"></td>
            </form>
        </tr>
    </table>

マイケル
マイケル
これも前回と同じく、一つのformでいくつかの処理で分けるので
submitボタンのnameを分けておきましょう!
エレキベア
エレキベア
お金の種類によって入金額を変えるクマね

・購入処理の記述
マイケル
マイケル
そして購入部分はこちらになります!
Screenshot 2020 11 12 23 24 54 2

    @if ($machine->juices != null)
        <ul class="juice-container">
            @foreach ($machine->juices as $juice)
                <li>
                    <form action="/buy" id="buyform_{{$juice->id}}" method="post">
                        @csrf
                        <input type="hidden" name="id" value="{{$juice->id}}">
                        <input type="hidden" name="m_id" value="{{$juice->m_id}}">
                        <input type="hidden" name="price" value="{{$juice->price}}">
                        <input type="hidden" name="money" value="{{$machine->money}}">
                        <img class="ju-image" src="data:image/jpeg;base64,{{$juice->ju_image}}" alt="image">
                        <div class="juice-name">{{$juice->name}}</div>
                        <div class="cold-msg">つめた〜い</div>
                        <div class="juice-price">{{$juice->price}}円</div>
                        <div class="push-button-area">
                        @if (0 < $juice->stock)
                            <input class="push-button" type="submit" value="PUSH">
                        @else
                            <input class="push-button-disabled" type="submit" value="SOLD" disabled>
                        @endif
                        </div>
                        <div class="juice-stock">在庫:{{$juice->stock}}個</div>
                    </form>
                </li>
            @endforeach
        </table>
    @endif

マイケル
マイケル
ここでMachineモデルの「hasMany()」で取得した結果を出力しているよ!
$machine->juicesと書くことで、juices()メソッドの戻り値が出力されるんだ!
エレキベア
エレキベア
Modelの時の説明してたやつクマね
これだけで結合結果が出力できるのはすごいクマね
マイケル
マイケル
それからBladeでは「@if」を使うことで判定条件も記述できるよ!
上の例では在庫(stock)が0の時にはボタンを非活性にしているんだ。
エレキベア
エレキベア
条件文が使えれば柔軟に表示処理を行えるクマね

Controller

マイケル
マイケル
そして最後に、Controllerの記述はこんな感じだ!

・index()メソッド -> 初期表示
・buy()メソッド -> 購入処理
・maney()メソッド -> 入金処理

というように分けているよ!
<?php

namespace App\Http\Controllers;

use App\Juice;
use App\Machine;
use App\Http\Requests\BuyRequest;
use Illuminate\Http\Request;

class BuyController extends Controller
{
    public function index(Request $request)
    {
        // クエリパラメタから対象の自動販売機を取得
        $machine = Machine::where('id', $request->id)->first();
        return view('buy.index', ['machine' => $machine]);
    }

    public function buy(BuyRequest $request)
    {
        // machineテーブルのmoneyを減らす
        Machine::where('id', $request->m_id)->decrement('money', $request->price);
        // juiceテーブルの在庫を減らす
        Juice::where('id', $request->id)->decrement('stock', 1);
        return redirect('/buy?id=' . $request->m_id);
    }

    public function money(Request $request)
    {
        if (isset($_POST['oturi'])) {
            // おつり返却処理
            // mochineテーブルのmoneyを0に更新
            Machine::where('id', $request->id)->update(['money'=>0]);
        } else {
            // 入金処理
            $input_money = 0;
            if (isset($_POST['10button'])) {
                $input_money = 10;
            } elseif (isset($_POST['100button'])) {
                $input_money = 100;
            } elseif (isset($_POST['500button'])) {
                $input_money = 500;
            } elseif (isset($_POST['1000button'])) {
                $input_money = 1000;
            }
            // mochineテーブルのmoneyに加算
            Machine::where('id', $request->id)->increment('money', $input_money);
        }
        return redirect('/buy?id=' . $request->id);
    }
}

・初期表示
マイケル
マイケル
初期表示の処理は下記のように記述していて、
自動販売機IDに紐づくデータのみ取得しています!

    public function index(Request $request)
    {
        // クエリパラメタから対象の自動販売機を取得
        $machine = Machine::where('id', $request->id)->first();
        return view('buy.index', ['machine' => $machine]);
    }
↑初期表示処理
マイケル
マイケル
こう書くことで、
http://【サーバアドレス】/buy?id=【自動販売機ID】
でアクセスすればその自販機の一覧のみ取得できるんだ!
エレキベア
エレキベア
リクエストにパラメータを渡しているわけクマね
Screenshot 2020 11 14 7 58 42
Screenshot 2020 11 14 7 58 59
↑自動販売機IDに紐づく飲み物が表示される

・購入、入金処理
マイケル
マイケル
購入処理は下記のように、
飲み物の価格の分だけお金を、在庫は1ずつ減らしています!

    public function buy(BuyRequest $request)
    {
        // machineテーブルのmoneyを減らす
        Machine::where('id', $request->m_id)->decrement('money', $request->price);
        // juiceテーブルの在庫を減らす
        Juice::where('id', $request->id)->decrement('stock', 1);
        return redirect('/buy?id=' . $request->m_id);
    }
↑購入処理
エレキベア
エレキベア
減らす処理はどこでやってるクマ?
マイケル
マイケル
decrement(【項目名】,【減らす数】)
のメソッドを使うことで減らすことができるんだ!
エレキベア
エレキベア
メソッドが用意されていると楽ちんクマね
マイケル
マイケル
そして入金処理は以下のとおり!
前回と同じくsubmitボタンの名前で分岐させています!


    public function money(Request $request)
    {
        if (isset($_POST['oturi'])) {
            // おつり返却処理
            // mochineテーブルのmoneyを0に更新
            Machine::where('id', $request->id)->update(['money'=>0]);
        } else {
            // 入金処理
            $input_money = 0;
            if (isset($_POST['10button'])) {
                $input_money = 10;
            } elseif (isset($_POST['100button'])) {
                $input_money = 100;
            } elseif (isset($_POST['500button'])) {
                $input_money = 500;
            } elseif (isset($_POST['1000button'])) {
                $input_money = 1000;
            }
            // mochineテーブルのmoneyに加算
            Machine::where('id', $request->id)->increment('money', $input_money);
        }
        return redirect('/buy?id=' . $request->id);
    }
↑入金処理
マイケル
マイケル
こっちは逆にincrement()メソッドで加算しているね。
エレキベア
エレキベア
簡潔にかけて綺麗クマ

・バリデーションの記述
マイケル
マイケル
そして次にバリデーションです!
これも前回と同じくFormRequestクラスを作成して設定しましょう!
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class BuyRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        if ($this->path() == 'buy')
        {
            return true;
        }
        return false;
    }

    /**
     * バリデーションルールの定義
     *
     * @return array
     */
    public function rules()
    {
        // お金が足りているかのチェック
        $remain = function($attribute, $value, $fail) {
            $input_data = $this->all();
            if ($input_data['money'] - $input_data['price'] <= 0)
            {
                $fail('【金額】お金が足りないでござる。');
            }
        };
        // バリデーション
        $price_validation = ['required', $remain];
        return [
            'id'=>'required',
            'm_id'=>'required',
            'price'=>$price_validation,
            'money'=>'required',
        ];
    }

    /**
     * エラーメッセージの定義
     * 
     * @return array
     */
    public function messages()
    {
        return [
            'id.required'=>'【ジュースID】ジュースIDが設定されていないでござる。',
            'm_id.required'=>'【自動販売機ID】自動販売機IDが設定されていないでござる。',
            'price.required'=>'【価格】価格が設定されていないでござる。',
            'money.required'=>'【金額】金額が設定されていないでござる。',
        ];
    }
}
エレキベア
エレキベア
今回もrule()メソッドに少し
個別の処理を入れてるクマね
マイケル
マイケル
その通り!
今回は複数項目の関連チェックを行うため、
下記チェックを追加しているよ!

        // お金が足りているかのチェック
        $remain = function($attribute, $value, $fail) {
            $input_data = $this->all();
            if ($input_data['money'] - $input_data['price'] <= 0)
            {
                $fail('【金額】お金が足りないでござる。');
            }
        };

マイケル
マイケル
このように記述すれば少し複雑なチェック処理も記述できるんだ!
エレキベア
エレキベア
オリジナルのチェック処理も入れ放題クマね

・Routeの設定
マイケル
マイケル
それでは最後に記述したControllerを呼び出すため
Routeの設定をしましょう!
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::get('edit', 'EditController@index');
Route::post('edit', 'EditController@edit');

Route::get('buy', 'BuyController@index');
Route::post('buy', 'BuyController@buy');
Route::post('buy/money', 'BuyController@money');
マイケル
マイケル
追加するのは下の三行!
今回は入金処理と購入処理を別のアクションにしているため、postを2つ記述しているよ!
マイケル
マイケル
以上で自動販売機システムの完成だーーー!!
エレキベア
エレキベア
やったクマ〜〜〜〜!!!!

おわりに

マイケル
マイケル
というわけで三回に渡る長編企画でしたが
いかがだったでしょうか??
エレキベア
エレキベア
早足だったクマが、一つのシステムを完成させて
自信がついたクマーー!!
マイケル
マイケル
それはよかった!
今回作ったシステムは最低限動作するけど、

 ・排他処理の実装
 ・ユーザ管理や権限周りの実装
 ・ページングの実装

あたりは省略しているから、次のステップアップを目指す人は挑戦してみてね!
エレキベア
エレキベア
まだまだシステム開発は先が長いクマね
マイケル
マイケル
知れば知るほど奥が深いWEBアプリ開発!
興味を持った方はぜひ挑戦してみてね!
エレキベア
エレキベア
クマ〜〜〜〜〜!!
マイケル
マイケル
それでは今日はこの辺で!
アデュー!!

【Laravel】第三回 自動販売機システムを作ろう! 〜購入画面の作成〜 〜完〜

コメント