1
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-02

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

今までの開発記録はこちらへ
胡蝶蘭を捨てるくらいならワイが欲しいので、サービス開発する編
公式ドキュメントの言う通り、パッケージをインストールされたら、Inertia.jsが導入されて???になった編
マルチログインを作ってみた編

管理者機能を作ります!

現在の開発状況

スクリーンショット (1633).png

今回の予定

  • ユーザー新規登録
  • ユーザー編集
  • ユーザー削除
  • 退会ユーザーの処理

ルート設定

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

全部をルートやコントローラーに書くのは面倒くさい!!

そんな悩みを解決するのが

リソースコントローラー!

詳しくはこちらへ
リソースコントローラー
スクリーンショット (1645).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ファイルが3つも必要?
face_angry_woman5.png

3つのファイルを一からデザインなんてやってられるか!!

internet_god.png

tailblocks「力が欲しいか?」

tailblocksにはtailwindで利用できるデザインのひな型があるのだ!
indexのトップにはこのひな形を改造して使います
スクリーンショット (1648).png
view codeをぽちっとすると
スクリーンショット (1651).png

pose_kandou_man.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

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

日時管理について

よく、ソーシャルゲームだと最新のログイン日とかありますよね
phpにはcarbonというライブラリがあり、これを利用して日付の表現ができます。

carbonのインストール

composer require nesbot/carbon

詳しくはこちらへ
PHPで日付時刻処理を書くならCarbonを使うべき

今回はログインから何日前かを表示したいので
diffForHumans()
を使用します。
ただし、一つ問題点が・・・
前回のログイン日が30日前までならどの日かイメージしやすいのですが、例えば334日前ならイメージできるのか?
study_yaruki_nai.png

30日以上の日付の計算ができない男

というわけで、30日以上なら、前回のログイン日を指定するように分岐させます

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
フォームはPUT/PATCH,DELETEメゾットをサポートしていない弱点がある。
そのために、フォームの下に
method('PUT')**などを追記する必要がある。

退会ユーザー管理機能

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

準備

softdeleteを使う

 $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')
    }

ポイントは2つだけ
onlyTrashed()で退会したユーザーの情報を入手し、
forceDelete()で物理削除をするということ

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

ここまでを終えて

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0