Laravel6.0で新規作成・編集・一覧・ページネーションを作成したメモ

## 概要
[前回](https://qiita.com/hibohiboo/items/34d3563a399512d74fe7)の続き。
[Laravel 6.0 基本のタスクリスト](https://qiita.com/ucan-lab/items/36f6e89abad26a68f69a)を参考に、CRUD操作の一部を作成した。
Bootstrapが4であったりという点が異なっている。

## データベース

タグを今回は作成する

```console
php artisan make:migration create_tags_table 
```

```php:whitemap/database/migrations/create_tags_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTagsTable extends Migration
{

    public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->integer('create_user_id')->unsigned()->comment('作成者ID');
            $table->string('name');
            $table->integer('value')->default(0);
            $table->boolean('is_display')->default(true);
            $table->timestamp('created_at')->useCurrent();
            $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP'));

            $table->foreign('create_user_id')->references('id')->on('users');
        });
    }
    public function down()
    {
        Schema::dropIfExists('tags');
    }
}
```

## アプリケーション

### モデル

```console
php artisan make:model Models/Tag 
```

上記コマンドで以下が作成される。
規約通りに作ると、Eloquentが自動的にO/Rマッピングしてくれる。

```php:whitemap/app/Models/Tag.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    //
}
```

### コントローラ

```
php artisan make:controller Admin/TagController --resource --model=Models/Tag 
```

上記コマンドでモデルと紐づいたコントローラが作成される。
ただし、今回は新規作成画面や編集画面をそれぞれ作らない予定なので、`create`や`edit`メソッドは割愛。

```php:whitemap/app/Http/Controllers/Admin/TagController.php
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Tag;
use App\Http\Requests\Admin\StoreTagPost;
use Illuminate\Http\Request;

class TagController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $tags = Tag::paginate(config('const.Paginator.PER_PAGE'));
        return view('admin/tags', [
            'tags' => $tags
        ]);
    }

    /**
     * Store a newly created resource in storage.
     * 新規作成
     *
     * @param  App\Http\Requests\Admin\StoreTagPost  $request
     * @return \Illuminate\Http\Response
     */
    public function store(StoreTagPost $request)
    {
        $user = $request->user();
        $tag = new Tag();
        $tag->name = $request->name;
        $tag->create_user_id = $user->id;
        $tag->value = $request->value;
        $tag->save();

        return redirect('/tag');
    }

    /**
     * Update the specified resource in storage.
     * 変更の保存
     * Laravelはタイプヒントされた変数名とルートセグメント名が一致する場合、
     * ルートかコントローラアクション中にEloquentモデルが定義されていると、自動的に依存解決する。
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Models\Tag  $tag
     * @return App\Http\Requests\Admin\StoreTagPost
     */
    public function update(StoreTagPost $request, Tag $tag)
    {
        $tag->name = $request->name;
        $tag->value = $request->value;
        $tag->save();

        return redirect('/tag');
    }
}
```

ページネーション用に、1ページに何ページ表示するかの数字を定数にしている。

```diff:whitemap/config/const.php
<?php

return [
    // Couponsで使う定数
    'Coupons' => [
        'TYPE_GET' => 1,
        'TYPE_USE' => 2,
    ],
+    'Paginator'=>[
+        'PER_PAGE'=>30
+    ]
];
```

#### バリデーション

```console
php artisan make:request Admin/StoreTagPost
```

バリデーションの設定を行う。
タイプヒントで指定することでコントローラに使用を伝える。
例：`update(StoreTagPost $request)`

```php:whitemap/app/Http/Requests/Admin/StoreTagPost.php
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;

class StoreTagPost extends FormRequest
{
    public function authorize()
    {
        // そもそも管理者しかタグの更新は行わないためここでは判定しない
        return true;
    }

    public function rules()
    {
        return [
            'name' => 'required|max:255',
            'value' => 'required|integer'
        ];
    }

    public function messages()
    {
        return [
            'name.required' => '名前は必須です',
            'name.max' => '名前は255文字以内で入力してください',
            'value.required'  => '値は必須です',
        ];
    }
}
```

### ルーティング

`{tag}`にプライマリーキーを受け取ることを想定して、コントローラで$tagで受け取ることで暗黙的なモデルバインディングを実現している。


```diff:whitemap/routes/web.php

Route::group(['middleware' => ['auth', 'can:admin-access']], function () {
    Route::get('/admin', function (Illuminate\Http\Request $request) {
        return view('admin/dashboard');
    });


+    Route::get('/tag','Admin\TagController@index');
+    Route::post('/tag','Admin\TagController@store');
+    Route::put('/tag/{tag}','Admin\TagController@update');
});
```

### ビュー

fontawesomeは公式の`<link href="https://use.fontawesome.com/releases/v5.11.2/css/all.css" rel="stylesheet">`よりもjsdelivrのほうが数段早い。

```diff:whitemap/resources/views/layoutes/app.blade.php
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="異世界漂流TRPG ドリフトサヴァイブはサバイバルをして文明を築き上げるTRPGです。" />
    <meta name="keywords" content="Laravel,laradock,gcp" />
    <meta name="robots" content="index" />
    @yield('meta')
    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>{{ config('app.name', 'Laravel') }} - @yield('title')</title>
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.11.2/css/all.min.css" integrity="sha256-+N4/V/SbAFiW1MPBCXnfnP9QSN3+Keu+NlB+0ev/YKQ=" crossorigin="anonymous">
    @yield('css')
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
+    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8=" crossorigin="anonymous"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
+    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script src="{{ mix('js/common/app.js') }}"></script>
    @yield('head-scripts')
    <!-- Google Tag Manager -->
    <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
    new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
    j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
    'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
    })(window,document,'script','dataLayer','GTM-PBSJTDV');</script>
    <!-- End Google Tag Manager -->
  </head>
  <body>
    <!-- Google Tag Manager (noscript) -->
    <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PBSJTDV" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
    <!-- End Google Tag Manager (noscript) -->
    <div id="app">
        <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
                @guest
                @else
                    <a class="navbar-brand user-icon" href="{{ url('/') }}">
                      <i class="user-icon">
                        <img src="{{\Auth::user()->twitter_profile_image_url_https}}">
                      </i>
                    </a>
                @endguest
            <!-- <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">
                <ul class="navbar-nav mr-auto">
                    <li class="nav-item {{str_replace(url('/'),'',url()->current()) === '/'     ? 'active' : ''}}"><a class="nav-link" href="{{ url('/') }}">トップ</span></a></li>
                    <li class="nav-item {{str_replace(url('/'),'',url()->current()) === '/home' ? 'active' : ''}}"><a class="nav-link" href="{{ url('/home') }}">マイページ</span></a></li>
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                        サイト情報
                        </a>
                        <div class="dropdown-menu" aria-labelledby="navbarDropdown">
                            <a class="dropdown-item" href="/agreement">利用規約</a>
                            <a class="dropdown-item" href="/privacy-policy">プライバシーポリシー</a>
                            <div class="dropdown-divider"></div>
                            <a class="dropdown-item" href="https://github.com/hibohiboo/whitemap">github</a>
                        </div>
                    </li>
                </ul>
            </div>
            @guest
              <div class="my-2 my-lg-0">
                <ul class="nav navbar-nav navbar-right">
                    <li class="nav-item"><a href="{{ route('login') }}" class="nav-link">ログイン</a></li>
                </ul>
              </div>
            @else
            
            @endguest
            
        </nav>
        @yield('content')
    </div>
    <!-- Scripts -->
    <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js" integrity="sha256-VeNaFBVDhoX3H+gJ37DpT/nTuZTdjYro9yBruHjVmoQ=" crossorigin="anonymous"></script>
    @yield('scripts')
  </body>
</html>
```

bootstrap3の`panel`がbootstrap4では`card`になっているなど微修正。


```php:whitemap/resouces/vies/admin/tags.blade.php
@extends('layouts.app')

@section('content')
    {{-- このコメントはレンダ後のHTMLには現れない --}}
    {{-- Bootstrapは一度に1つのモーダルウィンドウしかサポートしない。入れ子になったモーダルは、ユーザー経験が乏しいと思われるためサポートされていない。--}}
    {{-- 可能であれば、他の要素からの干渉を避けるために、モーダルHTMLを最上位に配置すること --}}
    <div id="editModal" class="modal fade" tabindex="-1" role="dialog">
      <form id="edit-form" action="{{ url('tag')}}" method="POST">
        @csrf
        @method('PUT')
        <div class="modal-dialog" role="document">
          <div class="modal-content">
            <div class="modal-header">
              <h5 class="modal-title">編集</h5>
              <button type="button" class="close" data-dismiss="modal" aria-label="閉じる">
                <span aria-hidden="true">&times;</span>
              </button>
            </div>{{-- /.modal-header --}}
            <div class="modal-body">
              {{-- タグ名 --}}
              <div class="form-group">
                <label for="edit-tag-name" class="col-sm-3 control-label">タグ名</label>
                <div class="col-sm-6">
                    <input type="text" name="name" id="edit-tag-name" class="form-control" value="{{ old('tag') }}">
                </div>
              </div>
              {{-- タグ値 --}}
              <div class="form-group">
                <label for="edit-tag-value" class="col-sm-3 control-label">値</label>
                <div class="col-sm-6">
                    <input type="number" name="value" id="edit-tag-value" class="form-control" value="{{ old('tag') }}">
                </div>
              </div>
            </div>{{-- /.modal-body --}}
            <div class="modal-footer">
              <button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button>
              <button type="submit" class="btn btn-primary">変更を保存</button>
            </div>{{-- /.modal-footer --}}
          </div>{{-- /.modal-content --}}
        </div>{{-- /.modal-dialog --}}
      </form>
    </div>{{-- /.modal --}}
    <main class="container">
        <div class="col-sm-offset-2 col-sm-8">
            <div class="card">
                <div class="card-header">新しいタグ</div>
                <div class="card-body">
                    {{-- バリデーションエラーの表示 --}}
                    @include('common.errors')

                    {{-- 新タグフォーム --}}
                    <form action="{{ url('tag')}}" method="POST" class="form-horizontal">
                        @csrf
                        {{-- タグ名 --}}
                        <div class="form-group">
                            <label for="tag-name" class="col-sm-3 control-label">タグ名</label>

                            <div class="col-sm-6">
                                <input type="text" name="name" id="tag-name" class="form-control" value="{{ old('tag') }}">
                            </div>
                        </div>

                        {{-- タグ値 --}}
                        <div class="form-group">
                            <label for="tag-value" class="col-sm-3 control-label">値</label>

                            <div class="col-sm-6">
                                <input type="number" name="value" id="tag-value" class="form-control" value="{{ old('tag') }}">
                            </div>
                        </div>

                        {{-- タグ追加ボタン --}}
                        <div class="form-group">
                            <div class="col-sm-offset-3 col-sm-6">
                                <button type="submit" class="btn btn-primary">
                                    <i class="fa fa-btn fa-plus"></i> タグ追加
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>

            @if (count($tags) > 0)
                <div class="card">
                    <div class="card-header">タグ一覧</div>
                    <div class="card-body">
                        <table class="table table-striped tag-table">
                            {{-- テーブルヘッダ --}}
                            <thead>
                                <tr>
                                    <th>タグ</th>
                                    <th>値</th>
                                    <th>&nbsp;</th>
                                </tr>
                            </thead>
                            {{-- テーブル本体 --}}
                            <tbody>
                              @foreach ($tags as $tag)
                                <tr>
                                  <td class="table-text">
                                      <div>{{ $tag->name }}</div>
                                  </td>
                                  <td class="table-text">
                                      <div>{{ $tag->value }}</div>
                                  </td>
                                  <td>
                                    <button type="button" class="btn" data-toggle="modal" data-target="#editModal" 
                                            data-action="{{ url('tag/' . $tag->id) }}" data-name="{{$tag->name}}" data-value="{{$tag->value}}"
                                    >
                                    <i class="fa fa-btn fa-edit"></i>
                                  </button>
                                  </td>
                                </tr>
                              @endforeach
                            </tbody>
                        </table>
                    </div>
                </div>
            @endif
            {{ $tags->links() }}
        </div>
    </div>
@endsection
@section('scripts')
  <script src="{{ mix('js/admin/tag/index.js') }}"></script>
@endsection
```

mixで呼び出すjs用の設定が以下。externalsで外部からリソースを取得することを表す。

```console
npm install --save-dev @types/bootstrap
```

```diff:whitemap/webpack.mix.js
const mix = require('laravel-mix');
mix.ts('resources/ts/common/app.ts', 'public/js/common')
    .sass('resources/sass/app.scss', 'public/css')
    .version();

mix.ts('resources/ts/welcome/index.ts', 'public/js/welcome').version();
mix.ts('resources/ts/home/index.ts', 'public/js/home').version();
mix.ts('resources/ts/login/index.ts', 'public/js/login').version();
mix.webpackConfig({
    externals: {
+        jquery: 'jQuery',
        firebase: 'firebase',
        firebaseui: 'firebaseui',
        axios: 'axios',
        lodash: 'lodash',
+        bootstrap: 'bootstrap',
+        popper: 'popper.js'
    }
});

+ mix.ts('resources/ts/admin/tag/index.ts', 'public/js/admin/tag').version();
```

```ts:resouces/admin/tag/index.ts
import { ModalEventHandler } from 'bootstrap';
$('#editModal').on('show.bs.modal', function(event: ModalEventHandler<HTMLElement>) {
    const target = event.relatedTarget;
    console.log('modal', target);
    if (target === undefined) {
        return;
    }
    const $button = $(target); // モーダル切替えボタン
    const action = $button.data('action'); // data-* 属性から情報を抽出
    const name = $button.data('name');
    const value = $button.data('value');

    // モーダルの内容を更新。ここではjQueryを使用するが、代わりにデータ・バインディング・ライブラリまたは他のメソッドを使用することも可能
    const $modal = $(this);
    $modal.find('#edit-tag-name').val(name);
    $modal.find('#edit-tag-value').val(value);
    $modal.find('#edit-form').attr('action', action);
});
```

```console 
npm run watch-poll
```


## 参考

[Laravel 6.0 基本のタスクリスト](https://qiita.com/ucan-lab/items/36f6e89abad26a68f69a)
[bootstrap3 -> 4](https://cccabinet.jpn.org/bootstrap4/components/buttons)
[@types/bootstrap](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/bootstrap/index.d.ts)
[fontawesome は公式より jsdeliver のほうが早い](https://www.jsdelivr.com/package/npm/@fortawesome/fontawesome-free?path=css)
[FontAwesome の読み込み速度を公式サイトと CDN サービスで比較してみた](https://blog.ver001.com/fontawesome_cdn_jsdelivr/)
[暗黙のモデル結合](https://readouble.com/laravel/6.0/ja/routing.html#route-model-binding)
[Laravel 6.0 データベース：ペジネーション](https://readouble.com/laravel/6.0/ja/pagination.html)
[Laravel 6.0 コントローラ](https://readouble.com/laravel/6.0/ja/controllers.html)
[Laravel 6.0 バリデーション](https://readouble.com/laravel/6.0/ja/validation.html)