LaravelとVue.jsを使った見積作成アプリ その1

<h2>はじめに</h2>
<p>この記事は私自身がLaravelとVue.jsを勉強する目的で学んだことをまとめたものです。</p>

<h2>この記事で作成するアプリケーションについて</h2>
<p>この記事は以下の環境で作成しました。</p>
<ul><li>Laravel 5.7.29</li>
<ul><li>PHP 7.3.11</li></ul>
<li>Vue.js 2.6.11</li></ul>

<h2>アプリケーションの全体像</h2>
<p>今回作成する見積作成アプリは全部で6画面あり、認証機能まで備えたアプリケーションを作成していきます。</p>
<p><a href="https://safe-badlands-35896.herokuapp.com/login">こちら</a>が完成した見積作成アプリです。</p>
<ul>
<li>見積一覧ページ</li>
<ul><li>このページでは作成した見積の一覧を表示します。</li></ul>
<li>見積編集ページ</li>
<ul><li>このページで見積の内容を編集、保存します。</li></ul>
<li>PDF表示ページ</li>
<ul><li>このページでは作成した見積をPDFで表示し、保存と印刷を可能にします。</li></ul>
<li>ログインページ</li>
<ul><li>ログインページも作成します。最終的にはログイン中のユーザーの見積のみ表示するように実装します。</li></ul>
<li>会員登録ページ</li>
<ul><li>会員登録ではメールアドレス、ユーザー名、パスワードを入力します。</li></ul>
<li>プロフィール編集ページ</li>
<ul><li>このページで見積に表示される自分の情報を編集できるようにします。</li></ul>
</ul>

<h2>テーブル定義</h2>
<p>見積テーブルと商品テーブルを作成します。二つのテーブルの関係性は見積一つに対し商品が多数紐づく「一対多」にします。</p>
<table>
<table>
    <thead>
        <tr>
            <th colspan="2">見積テーブル</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>ID</td>
            <td>id</td>
        </tr>
        <tr>
            <td>タイトル</td>
            <td>title</td>
        </tr>
        <tr>
            <td>納入場所</td>
            <td>location</td>
        </tr>
        <tr>
            <td>取引方法</td>
            <td>transaction</td>
        </tr>
        <tr>
            <td>有効期限</td>
            <td>effectiveness</td>
        </tr>
        <tr>
            <td>宛先</td>
            <td>customer</td>
        </tr>
        <tr>
            <td>納入期限</td>
            <td>deadline_at</td>
        </tr>
        <tr>
            <td>見積日</td>
            <td>estimated_at</td>
        </tr>
    </tbody>
</table>
<table>
    <thead>
        <tr>
            <th colspan="2">商品テーブル</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>ID</td>
            <td>id</td>
        </tr>
        <tr>
            <td>見積ID</td>
            <td>estimate_id</td>
        </tr>
        <tr>
            <td>商品名</td>
            <td>name</td>
        </tr>
        <tr>
            <td>単位</td>
            <td>unit</td>
        </tr>
        <tr>
            <td>数量</td>
            <td>quantity</td>
        </tr>
        <tr>
            <td>単価</td>
            <td>unit_price</td>
        </tr>
        <tr>
            <td>備考</td>
            <td>other</td>
        </tr>
    </tbody>
</table>

<h2>見積一覧ページの作成</h2>
<p>環境構築ができていてLaravelの初期画面が表示されている前提で進めます。</p>

<h3>データベースの接続設定</h3>
<p>まずは接続設定を.envで行います。estimateというデータベースを作成しています。環境構築にはHomesteadを使用しました。</p>
```
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=estimate
DB_USERNAME=homestead
DB_PASSWORD=secret
```

<h3>マイグレーションファイルとモデルクラスの作成</h3>
```
$ php artisan make:migration create_estimates_table --create=estimates
```
<p>作成されたファイルに記入していきます。</p>
```create_estimates_table.php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateEstimatesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('estimates', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title', 100)->nullable();
            $table->string('location', 100)->nullable();
            $table->string('transaction', 100)->nullable();
            $table->string('effectiveness', 100)->nullable();
            $table->string('customer', 100)->nullable();
            $table->string('deadline_at', 100)->nullable();
            $table->date('estimated_at')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('estimates');
    }
}
```
<p>テーブル名はestimatesとしました。格納したい物の名前の複数形にするのが一般的です。また、見積作成の途中で保存したい時や記入せずに作成したい場合に対応するため、nullable()でカラムにNULL値を許容しました。</p>
<p>マイグレーションを実行します。</p>
```
$ php artisan migrate
```
<p>次にモデルクラスを作成します。</p>
```
$ php artisan make:model Estimate
```
<p>appディレクトリにEstimateモデルが作成されます。</p>
```Estimate.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Estimate extends Model
{
    //
}
```
<p>Estimateモデルに記述はしていませんが、継承元であるModelクラスで様々な設定を読み取ってくれるらしいです。</p>
<p>これでデータを扱う準備ができたのですが、テストデータが入っていた方がコントローラーを書きやすいので、Seederを用いてデータを挿入します。</p>
```
$ php artisan make:seeder EstimatesTableSeeder
```
<p>runメソッドの中にデータを挿入するコードを記述します。ここでは3つの見積を作りました。</p>
```EstimatesTableSeeder.php
<?php

use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class EstimatesTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $user = DB::table('users')->first();

        $titles = ['2021年おめでとうセール', '商品見積の件', 'サンプル見積の件'];
        $customers = ['株式会社XXX', '株式会社YYY', '株式会社ZZZ'];

        foreach (array_map(NULL, $titles, $customers) as [ $title, $customer ]) {
            DB::table('estimates')->insert([
                'title' => $title,
                'user_id' => $user->id,
                'customer' => $customer,
                'created_at' => Carbon::now(),
                'updated_at' => Carbon::now(),
            ]);
        }
    }
}
```
<p>コマンドラインで実行します。</p>
```
$ php artisan db:seed --class=EstimatesTableSeeder
```
<p>「Database seeding completed successfully.」と返ってきたら成功です。</p>

<h3>ルーティングの設定</h3>
```web.php
Route::get('/estimates', 'EstimateController@index')->name('estimates.index');
```
<h3>コントローラークラスの作成</h3>
<p>コントローラークラスはコマンドラインから作成。</p>
```
$ php artisan make:controller EstimateController
```
<p>作成されたEstimateController.phpにindexメソッドを追加します。</p>
```EstimateController.php
use App\Estimate; // ★ 追加

    public function index()
    {
        $estimates = Estimate::all();

        return view('estimates/index', [
            'estimates' => $estimates,
        ]);
    }
```
<p>view関数でテンプレートにデータを渡し、その結果を返却しています。view関数の第一引数がテンプレートファイル名で第二引数がテンプレートに渡すデータです。</p>

<h3>テンプレートの作成</h3>
<p>テンプレートファイルを作成します。</p>
```
$ mkdir resources/views/estimates
$ touch resources/views/estimates/index.blade.php
$ touch resources/views/layout.blade.php
```
<p>layout.blade.phpとindex.blade.phpの中身は以下のように記述しました。</p>
```layout.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="csrf-token" content="{{ csrf_token() }}">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>見積作成アプリ</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
  <link rel="stylesheet" href="{{ asset('css/app.css') }}">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
</head>
<body>
  <header>
    <nav class="navbar navbar-expand-xs navbar-dark bg-dark p-1">
      <a class="navbar-brand" href="{{ route('estimates.index') }}">見積作成アプリ</a>
    </nav>
  </header>
  @yield('content')
  <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
  @yield('scripts')
</body>
</html>
```
```index.blade.php
@extends('layout')

@section('content')
<main>
  <div class="container">
    <div class="row">
      <div class="col col-md-12">
        <h2 class="text-center" style="padding-top:25px">見積一覧</h2>
        <table class="table table-bordered table-hover" style="table-layout:fixed;">
          <thead class="thead-dark">
            <tr>
              <th class="col">タイトル</th>
              <th class="col">見積もり期日</th>
              <th class="col">場所</th>
              <th class="col">宛先</th>
            </tr>
          </thead>
          <tbody>
            @foreach($estimates as $estimate)
              <tr>
                <td class="position-relative">
                  <a href="{{ route('estimates.edit', ['estimate' => $estimate->id]) }}" class="stretched-link">
                    {{ $estimate->title }}
                  </a>
                </td>
                <td>{{ $estimate->estimated_at }}</td>
                <td>{{ $estimate->location }}</td>
                <td>{{ $estimate->customer }}</td>
              </tr>
            @endforeach
          </tbody>
        </table>
      </div>
    </div>
  </div>
</main>
<footer class="fixed-bottom bg-dark">
  <nav class="my-navbar">
    <div class="container">
      <div class="row">
        <div class="col-md-3">
          <a href="#">
            <button>新規作成</button>
          </a>
        </div>
        <div class="col-md-3 offset-md-6">
          <a href="#">
            <button>プロフィール設定</button>
          </a>
        </div>
      </div>
    </div>
  </nav>
</footer>
@endsection
```
<p>テンプレートの中でも@を付ければPHPのようにforeachを使えます。この際、コントローラーから渡された$estimatesを参照しています。変数の値の展開は{{ }}のように波括弧二つで実現します。</p>
<p>CSSフレームワークにはBootstrapを使用しました。</p>

<h2>見積編集ページの作成</h2>
<h3>商品テーブルの作成</h3>
<p>まずは見積テーブルと同様にマイグレーションファイルを作成します。</p>
```
$ php artisan make:migration create_items_table --create=items
```
<p>マイグレーションファイルを記述します。</p>
```create_items_table.php
<?php

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

class CreateItemsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('estimate_id')->unsigned();
            $table->string('name', 100)->nullable();
            $table->string('unit', 10)->nullable();
            $table->integer('quantity')->nullable();
            $table->integer('unit_price')->nullable();
            $table->string('other', 100)->nullable();
            $table->timestamps();

            $table->foreign('estimate_id')->references('id')->on('estimates')->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('items');
    }
}
```
<p>ここでは外部キー制約を設定しています。外部キー制約は他のテーブルとの結びつきを表現するためのカラムに設定します。外部キー制約が設定されたカラムには、好き勝手な値は入れられなくなります。今回の例で言うと、商品テーブルの見積ID列には実際に存在する見積IDの値しか入れることができなくなります。これによりデータの不整合を防ぎます。また、onDelete('cascade')により見積テーブルのデータを削除した場合、商品テーブル内の一致するデータを自動的に削除してくれます。</p>
<p>マイグレーションを実行します。</p>
```
$ php artisan migrate
```
<p>続けて商品テーブルに対応するモデルクラスを作成します。</p>
```
$ php artisan make:model Item
```
```Item.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Item extends Model
{
    //
}
```
<p>テストデータを挿入するためにシーダーを作成します。</p>
```
$ php artisan make:seeder ItemsTableSeeder
```
```ItemsTableSeder.php
<?php

use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class ItemsTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $names = ['ガードレール', 'エムコール', '塩化カルシウム'];
        $units = ['式', '袋', '袋'];
        $quantities = [1, 10, 25];
        $unit_prices = [150000, 5000, 1000];

        foreach (array_map(NULL, $names, $units, $quantities, $unit_prices) as [ $name, $unit, $quantity, $unit_price ]) {
            DB::table('items')->insert([
                'estimate_id' => 1,
                'name' => $name,
                'unit' => $unit,
                'quantity' => $quantity,
                'unit_price' => $unit_price,
            ]);
        }
    }
}
```
<p>今回はID=1の見積に対して3つの商品を登録しました。</p>
```
$ php artisan db:seed --class=ItemsTableSeeder
```

<h3>ルーティングの設定</h3>
```web.php
Route::get('/estimates/edit', 'EstimateController@showEditForm')->name('estimates.edit');
Route::post('/estimates/edit', 'EstimateController@edit');
Route::get('/estimates/create', 'EstimateController@create')->name('estimates.create');
Route::post('/estimates/create', 'EstimateController@create');
```

<h3>コントローラーの作成</h3>
<p>コントローラーを書いていきます。既存の見積もりを編集する場合はそのままshowEditFormへ、新規作成の場合はcreate->showEditFormと推移します。</p>
```EstimateController.php
use App\Item; // ★ 追加

    public function showEditForm(Request $request)
    {
        $estimate_id = $request->input('estimate');
        $estimate = Estimate::find($estimate_id);

        return view('estimates/edit', [
            'estimate' => $estimate,
        ]);
    }

    public function create()
    {
        $estimate = new Estimate();
        $estimate->save();

        return redirect()->route('estimates.edit', [
            'estimate' => $estimate->id,
        ]);
    }

    public function edit(Request $request)
    {
        $estimate_id = $request->input('estimate');
        $current_estimate = Estimate::find($estimate_id);

        $current_estimate->title = $request->title;
        $current_estimate->location = $request->location;
        $current_estimate->transaction = $request->transaction;
        $current_estimate->effectiveness = $request->effectiveness;
        $current_estimate->customer = $request->customer;
        $current_estimate->deadline_at = $request->deadline_at;
        $current_estimate->estimated_at = $request->estimated_at;

        $current_estimate->save();

        return redirect()->route('estimates.edit', [
            'estimate' => $estimate_id
        ]);
    }
```
<p>コントローラーメソッドの引数にRequestクラスのインスタンスを受け入れる記述をすることでユーザーの入力値をRequestクラスのインスタンス$requestに詰めて引数として渡してくれます。Requestクラスのインスタンスにはリクエストヘッダや送信元IPなどいろいろな情報が含まれていますが、その中にフォームの入力値も入っています。</p>
```
$request->title;
```
<p>リクエスト中の入力値は上記のようにプロパティとして取得することができます。</p>
<p>また、クエリパラメータの取得にはRequestクラスのinputメソッドを使用します。inputメソッドの第一引数へ、クエリパラメータのキーを指定します。今回指定するクエリパラメータのキーは'estimate'です。この為、inputメソッドの第一引数には'estimate'を指定します。</p>
<p>次のポイントはデータベースに書き込む処理です。データベースへの書き込みは以下の手順で実装します。</p>
<ol>
<li>モデルクラスのインスタンスを作成する。</li>
<li>インスタンスのプロパティに値を代入する。</li>
<li>saveメソッドを呼び出す。</li>
</ol>
<p>これにより、モデルクラスが表すテーブルに対してINSERTが実行されます。</p>

<h2>次回</h2>
<p>ここまで、見積一覧ページと見積編集ページのコントローラーを作成しました。<a href="https://qiita.com/yaaamaaadaaa/items/0465b750536fa352fac5">次回はVue.jsを利用した見積編集ページのテンプレートを作成</a>します。</p>