laravel学んで2か月で自サービスを開発した話 Part4

みなさん、こんにちは。
見切り発車で開発記録を載せていますが、part20超えそうな気がしてきます

今までの開発記録はこちらへ
<a href="https://qiita.com/k-hayack884/items/e571138e8bf7f35f7502">胡蝶蘭を捨てるくらいならワイが欲しいので、サービス開発する編</a>
<a href="https://qiita.com/k-hayack884/items/720c7a6300139317deac">公式ドキュメントの言う通り、パッケージをインストールされたら、Inertia.jsが導入されて？？？になった編</a>
<a href="https://qiita.com/k-hayack884/items/dccb9d479bd162144559">マルチログインを作ってみた編</a>

## 管理者機能を作ります!
### 現在の開発状況
![スクリーンショット (1633).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/8d293cd9-6f05-1887-802a-f3dbd1f9658a.png)

### 今回の予定
- ユーザー新規登録
- ユーザー編集
- ユーザー削除
- 退会ユーザーの処理

### ルート設定
必要なのはトップ画面のindex,ユーザー作成のcreate,作成処理のstore,ユーザー編集のedit,編集処理のupdate,ユーザー削除のdeleteが必要ということだけど

### 全部をルートやコントローラーに書くのは面倒くさい！！
そんな悩みを解決するのが
### リソースコントローラー！
詳しくはこちらへ
<a href="https://readouble.com/laravel/8.x/ja/controllers.html">リソースコントローラー</a>
![スクリーンショット (1645).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/21f92340-683e-33a4-42d7-90b5bafb2cd3.png)
これがあれば、C（作成）R（読み取り）U（更新）D（削除）のルートの設定やコントローラーの関数の利用をかってにやってくれるのですごく便利です！

使い方は通常のコントローラー作成コマンドに--resource とつけるだけです。
というわけで、ぽちっとな　
```
php artisan make:controller AdminController --resource
```
そうなると
```AdminController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class AdminController extends Controller
{

    public function index()
    {
        //
    }

    public function create()
    {
        //
    }

    public function store(Request $request)
    {
        //
    }

    public function show($id)
    {
        //
    }

    public function edit($id)
    {
        //
    }

    public function update(Request $request, $id)
    {
        //
    }

    public function destroy($id)
    {
        //
    }
}
```
### やったぜ。
#### ルート設定もとても簡単で
```admin.php
Route::resource('users', AdminController::class)
    ->middleware('auth:admin')->except(['show']);
```
#### これだけ
showメゾットは今回は使わないため、取り除いています。

## コントローラー作成
では、さっき作ってもらったコントローラーに処理を書いていきます

```AdminController.php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Admin;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;

class AdminController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

    public function index()
    {
        $users = User::select('id', 'name', 'email', 'created_at')->paginate(5);
        return view('admin.users.index', compact('users'));
    }

    public function create()
    {
        return view('admin.users.create');
    }

    public function store(Request $request)
    {
        $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'confirmed', 'string', Password::defaults()],
        ]);

        User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);
        return redirect()->route('admin.users.index')
    }

    public function show($id)
    {
    }

    public function edit($id)
    {
        $user = User::findOrFail($id);
        return view('admin.users.edit', compact('user'));
    }

    public function update(Request $request, $id)
    {
        $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'confirmed', 'string', Password::defaults()],
        ]);
        $user = User::findOrFail($id);
        $user->name = $request->name;
        $user->email = $request->email;
        $user->password = Hash::make($request->password);
        $user->save();
        return redirect()->route('admin.users.index')
    }

    public function destroy($id)
    {
        User::findOrFail($id)->delete();
        return redirect()->route('admin.users.index')
    }
}
```
最初に、 ** $this->middleware('auth') **で認証している状態で使うことを設定する
データベースから変数をviewファイルに送りたいときは**compact関数**を使えばいいよ！

## viewの設定
もしかしてviewファイルが３つも必要？
![face_angry_woman5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/d8a8d572-83cb-1670-2e41-df4e122e6226.png)
### ３つのファイルを一からデザインなんてやってられるか！！
![internet_god.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/8b322117-f89d-27ab-8bbf-df5480893e74.png)
### <a href="https://tailblocks.cc/">tailblocks「力が欲しいか？」</a>

tailblocksにはtailwindで利用できるデザインのひな型があるのだ！
indexのトップにはこのひな形を改造して使います
![スクリーンショット (1648).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/1fb195aa-6f06-05cf-a3f0-b9797141f2e3.png)
view codeをぽちっとすると
![スクリーンショット (1651).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/18f4b58f-3c0b-0513-a32c-42221bf6463c.png)

![pose_kandou_man.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/80dd7cb0-578d-16ff-ca65-684daadc324e.png)
### 感動

あとはindex.blade.phpのファイルを作成し張り付けるだけ！
ということでコントローラーから得た情報を表示するために、さくっと改造します

```index.blade.php
<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            ユーザー管理
        </h2>
    </x-slot>
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="md:p-6 bg-white border-b border-gray-200">
                    <div class="container md:px-5 mx-auto">
                        <div class="flex justify-between w-full mb-4">
                            <h1 class="text-4xl font-medium title-font text-gray-900  ">ユーザー管理</h1>
                            <div class="p-2 w-1/3">
                                <button onclick="location.href='{{ route('admin.users.create') }}'"
                                    class="  bg-indigo-500 border-0  sm:px-8 focus:outline-none hover:bg-indigo-600 rounded text-lg py-2 text-white">新規登録する</button>
                            </div>
                        </div>
                        <div class="lg:w-3/4 w-full mx-auto overflow-auto">
                            <table class="table-auto text-left whitespace-no-wrap border solid">
                                <thead>
                                    <tr>
                                        <th
                                            class="md:px-4 py-3 w-1/6 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100">
                                            ユーザー名</th>
                                        <th
                                            class=" md:px-4 py-3 w-1/3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100 rounded-tl rounded-bl">
                                            メールアドレス</th>
                                        <th
                                            class="md:px-4 py-3 w-1/6 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100">
                                            前回のログイン</th>
                                        <th
                                            class="w-10 title-font w-1/3 tracking-wider font-medium text-gray-900 text-sm bg-gray-100 rounded-tr rounded-br">
                                        </th>
                                    </tr>
                                </thead>
                                <tbody>
                                    @foreach ($users as $user)
                                        <tr class="border-2">
                                            <td class="md:px-4 py-3 break-all">{{ $user->name }}</td>
                                            <td class="md:px-4 py-3 break-all">{{ $user->email }}</td>
                                            <td class="md:px-4 py-3">
                                                @if ($user->created_at->diffInDays(now()) >= 30)
                                                    {{ $user->created_at->subDays(30)->toDateString() }}
                                                @else
                                                    {{ $user->created_at->diffForHumans() }}
                                                @endif
                                            </td>
                                            <td class=" text-center md:px-4 py-3 md:flex md:flex-row">
                                                <button type="button"
                                                    onclick="location.href='{{ route('admin.users.edit', ['user' => $user->id]) }}'"
                                                    class="mx-auto text-white bg-indigo-500 border-0 py-2 px-4 focus:outline-none hover:bg-gray-600 rounded text-sm mb-2 md:mb-0">編集</button>
                                                <form id="delete_{{ $user->id }}" method="post"
                                                    action="{{ route('admin.users.destroy', ['user' => $user->id]) }}">
                                                    @csrf
                                                    @method('delete')
                                                    <a href="#" data-id="{{ $user->id }}" onclick="deletePost(this)"
                                                        class="inline-block mx-auto text-white bg-red-500 border-0 py-2 px-4 focus:outline-none hover:bg-red-600 rounded text-sm">
                                                        削除</a>
                                                </form>
                                            </td>
                                        </tr>
                                    @endforeach
                                </tbody>
                            </table>
                            {{ $users->links() }}
                        </div>
                        </section>
                    </div>
                </div>
            </div>
        </div>
        <script src="{{ mix('/js/app.js') }}"></script>
        <script>
            'use strict';

            function deletePost(e) {
                if (confirm('本当に削除してもよろしいですか？')) {
                    document.getElementById('delete_' + e.dataset.id).submit();
                }
            }
        </script>
</x-app-layout>
```
### プレビュー
![スクリーンショット (1653).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/629b2b82-e385-6f6a-56a1-db2686938af9.png)

@foreachを利用して、データベースからとった情報を吐き出しています。

## 日時管理について
よく、ソーシャルゲームだと**最新のログイン日**とかありますよね
phpには**carbon**というライブラリがあり、これを利用して日付の表現ができます。
#### carbonのインストール
```
composer require nesbot/carbon
```
詳しくはこちらへ
<a herf="https://qiita.com/yudsuzuk/items/ff894bd0b76d4657741d">PHPで日付時刻処理を書くならCarbonを使うべき</a>

今回はログインから何日前かを表示したいので
```diffForHumans()```
を使用します。
ただし、一つ問題点が・・・
前回のログイン日が３０日前までならどの日かイメージしやすいのですが、例えば３３４日前ならイメージできるのか？
![study_yaruki_nai.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/4e63d43a-bcb8-c5d1-3e1d-950603066da4.png)
### ３０日以上の日付の計算ができない男
というわけで、３０日以上なら、前回のログイン日を指定するように分岐させます

```index.php
  @if ($user->created_at->diffInDays(now()) >= 30)
      {{ $user->created_at->subDays(30)->toDateString() }} //〇年〇月〇日と表示
  @else
      {{ $user->created_at->diffForHumans() }}　　　　　　　//〇〇前と表示
  @endif
```

## create,editのview
ひな形はtailblocksから拝借
フォームの内容
```edit.blade.php
<form action="{{ route('admin.users.update', ['user' => $user->id]) }}" method="post">
                                    @method('PUT')
                                    @csrf
                                    <div class="flex flex-col items-center m-2">
                                        <div class="p-2 mx-auto">
                                            <div class="relative">
                                                <label for="name" class="leading-7 text-sm text-gray-600">ユーザー名</label>
                                                <input type="text" id="name" name="name"
                                                    class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
                                                    value="{{ $user->name }}" required>
                                            </div>
                                        </div>
                                        <div class="p-2 mx-auto">
                                            <div class="relative">
                                                <label for="email"
                                                    class="leading-7 text-sm text-gray-600">メールアドレス</label>
                                                <input type="email" id="email" name="email"
                                                    class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
                                                    value="{{ $user->email }}" required>
                                            </div>
                                        </div>
                                        <div class="p-2 mx-auto">
                                            <div class="relative">
                                                <label for="password"
                                                    class="leading-7 text-sm text-gray-600">パスワード</label>
                                                <input type="password" id="password" name="password"
                                                    class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
                                                    required>
                                            </div>
                                        </div>
                                        <div class="p-2 mx-auto">
                                            <div class="relative">
                                                <label for="password_confirmation"
                                                    class="leading-7 text-sm text-gray-600">パスワード確認</label>
                                                <input type="password" id="password_confirmation"
                                                    name="password_confirmation"
                                                    class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
                                                    required>
                                            </div>
                                        </div>
                                    </div>
                                    <div class="flex flex-col md:flex-row justify-around mt-4">
                                        <button type="button"
                                            onclick="location.href='{{ route('admin.users.index') }}'"
                                            class="mx-auto  text-white bg-gray-500 border-0 py-2 px-12 focus:outline-none hover:bg-gray-600 rounded text-lg mx-4 mb-4 md:mb-0">戻る</button>
                                        <button type="submit"
                                            class=" mx-auto  text-white bg-indigo-500 border-0 py-2 px-12 focus:outline-none hover:bg-indigo-600 rounded text-lg mx-4">更新</button>
                                    </div>
                                </form>
```
特に重要なのが**method('PUT')**の部分
便利なリソースコントローラーなんだけれど、
![スクリーンショット (1645).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/21f92340-683e-33a4-42d7-90b5bafb2cd3.png)
フォームはPUT/PATCH,DELETEメゾットをサポートしていない弱点がある。
そのために、フォームの下に**method('PUT')**などを追記する必要がある。

## 退会ユーザー管理機能
登録もする人もいれば、退会する人もいる。登録した人は管理するのは当然だが、退会した人を放っておいていいわけではない。
退会が、単なるユーザーの誤爆だったりすることもあるし、退会したユーザーがトラブルを起こしていたということもある。
もしかすると、警察などから情報提供を依頼されることもあるので、退会者の情報を保持することに越したことはないのである。

### 準備
softdeleteを使う
```_create_users_table
 $table->softDeletes(); //追加
```
この状態でマイグレーションするとテーブルに**deleted_at**が追加される。
このカラムはユーザーが退会してもユーザーをテーブルから削除せず、退会した時間をderete_atにスタンプする
ユーザー側から見ると削除されているように見えるが、データベースには残っていることである（論理削除）
一方で、ユーザーをテーブルから完全に抹消する方法もある（物理削除）
物理削除を使う機会はないが、コードを書く

### まずはモデルファイル
```User.php
use Illuminate\Database\Eloquent\SoftDeletes; //追加

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable, SoftDeletes; //追加
```
### ルート
```admin.php
Route::prefix('expired-users')
    ->middleware('auth:admin')->group(function () {
        Route::get('index', [AdminController::class, 'expiredUserIndex'])->name('expired-users.index');
        Route::post('destroy/{user}', [AdminController::class, 'expiredUserDestroy'])->name('expired-users.destroy');
    });
```

### コントローラーはAdminControllerを使いまわし
```AdminController.php
public function expiredUserIndex()
    {
        $expireUsers = User::onlyTrashed()->get();
        return view('admin.expired-users', compact('expireUsers'));
    }

    public function expiredUserDestroy($id)
    {
        User::onlyTrashed()->findOrFail($id)->forceDelete();
        return redirect()->route('admin.expired-users.index')
    }
```
ポイントは２つだけ
onlyTrashed()で退会したユーザーの情報を入手し、
forceDelete()で物理削除をするということ

viewはユーザー一覧のindex.phpを使いまわして、少しいじればＯＫ

## ここまでを終えて
物理削除のコマンドは正直いるか迷った
管理者による誤爆や、隠蔽もできそうだし
どの管理者が退会ユーザーを処理したのか、実装する必要があるかもしれない

これで一旦、管理者機能は実装できたので、次回はユーザーの機能を作っていきたい。