Open24

【挑戦】Laravel 8 で簡易的な掲示板を作ってみた

DaiNakaDaiNaka

はじめに

シリーズ化ではないですが、開発の手を止める事なくインプット&アウトプットを繰り返していく事で自身の技術力アップを図っていく事を目的に挑戦していく過程を収めていきたいと思います。
折角なので、アウトプットについても分かりやすくを意識していこうと思います。

前回の挑戦

https://zenn.dev/dainaka/scraps/6eafb7c86dad16

開発環境

  • XAMPP v3.3.0
  • composer 2.1.3
  • VS Code

利用言語

  • PHP
  • Laravel 8
  • tailwind

公開コード[GitHub]

https://github.com/DaiNaka1207/bbs-app

DaiNakaDaiNaka

企画

昔ながらのCGIで作成された様な簡易的な掲示板を作っていこうと思います。

完成イメージの参考サイト

https://www.kent-web.com/bbs/yybbs/yybbs.cgi

機能

  • ログインする必要がなく、直ぐに投稿する事ができる
  • 投稿した本人であればスレッドを削除する事ができる
  • 投稿されたスレッドに対して返信する事ができる
  • 検索をする事ができる
DaiNakaDaiNaka

プロジェクトの作成

まずは、コマンドプロンプトにて、下記のコードを実行してプロジェクトの作成からGitリポジトリの初期化を実施します。
laravel new bbs-app --git --branch="main"

次に、下記のコードを実行してGitHubと連携します。
git remote add origin https://github.com/DaiNaka1207/bbs-app.git

最後に、下記のコマンドを実行してリモートリポジトリへプッシュして準備完了です。
git push -u origin main

DaiNakaDaiNaka

環境設定

.env
- APP_NAME=laravel
+ APP_NAME=bbs-app
config/app.php
- 'timezone' => 'UTC',
+ 'timezone' => 'Asia/Tokyo',

- 'locale' => 'en',
+ 'locale' => 'ja',

- 'faker_locale' => 'en_US',
+ 'faker_locale' => 'ja_JP',

最後に、.envファイル内のDB_DATABASE=bbs_appを参考にデータベースを作成します。

DaiNakaDaiNaka

Tailwindcssのインストール

今回も、前回同様にTailwindcssを使っていきたいと思いますので、SEの休日さんのページを参考にインストールしていきます。
https://uedive.net/2021/5608/laravel8x-tailwind/

この環境設定のところは、理解して自分自身で設定ができる様になると良いと思いますが、今回の趣旨からは外れてしまう為、深堀りはしないつもりです。
ただ、いつかこういった設定も自身でできる様になり、説明もできる様になりたいと思っています。

DaiNakaDaiNaka

掲示板ページのビュー作成とルーティング設定

掲示板ページのビューを作成してルーティング設定するまでを書いていこうと思います。

ビューの作成

作成場所:resources\views\bbs\index.blade.php

index.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    
    <title>{{ env('app_name') }}</title>
    
    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
    <script src="{{ asset('js/app.js') }}"></script>
</head>
<body>
    <p class="text-blue-500">^^)v Hello World</p>
</body>
</html>

ルーティングの設定

welcomeページは削除して、サイトトップページを開こうとすると掲示板のトップページにリダイレクトしています。

web.php
- Route::get('/', function () {
-     return view('welcome');
- });

+ Route::redirect('/', '/bbs');
+ Route::view('/bbs', '/bbs/index');

ここまでで、http://localhost:8000へアクセスすると、下記画面が表示されます。

DaiNakaDaiNaka

掲示板ページの画面構成をコーディング

ここではフロントエンド部分コーディングをしていこうと思います。
まずは、ベタ打ちのテキストやフォームをコーディングしていきます。
index.blade.phpを下記の通り編集します。

index.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>{{ env('app_name') }}</title>
    
    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
    <script src="{{ asset('js/app.js') }}"></script>

    <style>
        .link-hover:hover {opacity: 70%;}
    </style>
</head>
<body class="bg-blue-100">
    <div class="w-11/12 max-w-screen-md m-auto">

        {{-- タイトル --}}
        <h1 class="text-xl font-bold mt-5">{{ env('app_name') }}</h1>

        {{-- 入力フォーム --}}
        <div class="bg-white rounded-md mt-5 p-3">
            <form action="/" method="POST">
                @csrf
                <div class="flex">
                    <p class="font-bold">名前</p>
                    <input class="border rounded px-2 ml-2" type="text" name="user_name">
                </div>
                <div class="flex mt-2">
                    <p class="font-bold">件名</p>
                    <input class="border rounded px-2 ml-2 flex-auto" type="text" name="message_title">
                </div>
                <div class="flex flex-col mt-2">
                    <p class="font-bold">本文</p>
                    <textarea class="border rounded px-2" name="message"></textarea>
                </div>
                <div class="flex justify-end mt-2">
                    <input class="my-2 px-2 py-1 rounded bg-blue-300 text-blue-900 font-bold link-hover cursor-pointer" type="submit" value="投稿">
                </div>
            </form>
        </div>

        {{-- 検索フォーム --}}
        <div class="bg-white rounded-md mt-3 p-3">
            <form action="/" method="post">
                @csrf
                <div class="mx-1 flex">
                    <input class="border rounded px-2 flex-auto" type="text" name="serch_message">
                    <input class="ml-2 px-2 py-1 rounded bg-gray-500 text-white font-bold link-hover cursor-pointer" type="submit" value="検索">
                </div>
            </form>
        </div>

        {{-- ページネーション --}}
        <p class="flex justify-center text-blue-300 mt-5 link-hover cursor-pointer">prev 1 2 3 4 next</p>

        {{-- 投稿 --}}
        <div class="bg-white rounded-md mt-1 mb-5 p-3">
            {{-- スレッド --}}
            <div>
                <p class="mb-2 text-xs">2021/11/20 18:00 @Noname</p>
                <p class="mb-2 text-xl font-bold">●●について</p>
                <p class="mb-2">これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。</p>
            </div>
            {{-- 削除ボタン --}}
            <form class="flex justify-end mt-5" action="/" method="POST">
                @csrf
                <input class="border rounded px-2 flex-auto" type="text" name="reply_message">
                <input class="px-2 py-1 ml-2 rounded bg-green-600 text-white font-bold link-hover cursor-pointer" type="submit" value="返信">
                <input class="px-2 py-1 ml-2 rounded bg-red-500 text-white font-bold link-hover cursor-pointer" type="submit" value="削除">
            </form>
            {{-- 返信 --}}
            <hr class="mt-2 m-auto">
            <div class="flex justify-end">
                <div class="w-11/12">
                    <div>
                        <p class="mt-2 text-xs">2021/11/20 19:00 @Noname</p>
                        <p class="my-2 text-sm">これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。</p>
                    </div>
                </div>
            </div>
        </div>

        {{-- 投稿 --}}
        <div class="bg-white rounded-md mt-1 mb-1 p-3">
            {{-- スレッド --}}
            <div>
                <p class="mb-2 text-xs">2021/11/20 18:00 @Noname</p>
                <p class="mb-2 text-xl font-bold">●●について</p>
                <p class="mb-2">これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。</p>
            </div>
            {{-- 削除ボタン --}}
            <form class="flex justify-end mt-5" action="/" method="POST">
                @csrf
                <input class="border rounded px-2 flex-auto" type="text" name="reply_message">
                <input class="px-2 py-1 ml-2 rounded bg-green-600 text-white font-bold link-hover cursor-pointer" type="submit" value="返信">
                <input class="px-2 py-1 ml-2 rounded bg-red-500 text-white font-bold link-hover cursor-pointer" type="submit" value="削除">
            </form>
            {{-- 返信 --}}
            <hr class="mt-2 m-auto">
            <div class="flex justify-end">
                <div class="w-11/12">
                    <div>
                        <p class="mt-2 text-xs">2021/11/20 19:00 @Noname</p>
                        <p class="my-2 text-sm">これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。</p>
                    </div>
                    <hr class="mt-2 m-auto">
                    <div>
                        <p class="mt-2 text-xs">2021/11/20 19:00 @Noname</p>
                        <p class="my-2 text-sm">これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。</p>
                    </div>
                </div>
            </div>
        </div>

        {{-- ページネーション --}}
        <p class="flex justify-center text-blue-300 mt-1 mb-5 link-hover cursor-pointer">prev 1 2 3 4 next</p>
    </div>
</body>
</html>

ここまでで、http://localhost:8000へアクセスすると、下記画面が表示されます。

DaiNakaDaiNaka

データベースの構成を検討

[thread]スレッドテーブル

カラム名 タイプ
id bigint(20)
user_name varchar(20)
user_identifier varchar(20)
message_title varchar(50)
message varchar(200)
created_at timestamp
update_at timestamp

[reply]リプライテーブル

カラム名 タイプ
id bigint(20)
thread_id int(20)
user_name varchar(20)
message varchar(200)
created_at timestamp
update_at timestamp
DaiNakaDaiNaka

テーブルの作成

データベースの構成を考えたら、マイグレーションファイルを作成してテーブルの作成を実行していきます。

[threads]テーブルの作成

コマンド:php artisan make:migration create_threads_table
生成場所:database\migrations\yyyy_mm_dd_hhmmss_create_threads_table.php

yyyy_mm_dd_hhmmss_create_threads_table.php
<?php

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

class CreateThreadsTable extends Migration
{
    public function up()
    {
        Schema::create('threads', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('threads');
    }
}

[replies]テーブルの作成

コマンド:php artisan make:migration create_replies_table
生成場所:database\migrations\yyyy_mm_dd_hhmmss_create_replies_table.php

yyyy_mm_dd_hhmmss_create_replies_table.php
<?php

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

class CreateRepliesTable extends Migration
{
    public function up()
    {
        Schema::create('replies', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('replies');
    }
}

カラムの設定

それぞれのファイルを更新して、カラムの設定をしていきます。

yyyy_mm_dd_hhmmss_create_threads_table.php
    public function up()
    {
        Schema::create('threads', function (Blueprint $table) {
-           $table->id();
+           $table->bigIncrements('id');
+           $table->string('user_name', 20);
+           $table->string('message_title', 50);
+           $table->string('message', 200);
            $table->timestamps();
        });
    }
yyyy_mm_dd_hhmmss_create_replies_table.php
    public function up()
    {
        Schema::create('replies', function (Blueprint $table) {
-           $table->id();
+           $table->bigIncrements('id');
+           $table->integer('thread_id');
+           $table->string('user_name', 20);
+           $table->string('message', 200);
            $table->timestamps();
        });
    }

マイグレーション

設定が完了したら、下記のコマンドにてマイグレーションを実行してテーブルを作成します。
php artisan migrate

[threads]テーブル

[replies]テーブル

DaiNakaDaiNaka

コントローラーの作成

ここでは、データベースを扱うコントローラーを作成していきます。
コマンド:php artisan make:controller ThreadController --resource
生成場所:app\Http\Controllers\ThreadController.php

ThreadController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ThreadController 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)
    {
        //
    }
}

コマンド:php artisan make:controller ReplyController --resource
生成場所:app\Http\Controllers\ReplyController.php

ReplyController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ReplyController 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)
    {
        //
    }
}
DaiNakaDaiNaka

コントローラーを経由したビュー表示へ変更

ここまででは、ルーティングでビューを表示する様に設定していましたが、コントローラーを経由したビュー表示へ切り替えていきます。

ThreadController.php
    public function index()
    {
-       //
+       // 掲示板ページを表示
+       return view('bbs/index');
    }
web.php
+ use App\Http\Controllers\ThreadController;
- Route::view('/bbs', '/bbs/index');
+ Route::resource('/bbs', ThreadController::class);
DaiNakaDaiNaka

データベースとの連携と表示(スレッド)

ここまででは、ベタ打ちのテキストを表示させていましたが、データベースから値を取得して表示する様にしていきます。

補足

  • データベースにテストデータを登録済みです。
  • リプライ情報は、まだベタ打ちのテキストを表示しています。
ThreadController.php
    public function index()
    {
        // スレッド情報を取得して代入
+       $threads = Thread::all();
        
        // 掲示板ページを表示
-       return view('bbs/index');
+       return view('bbs/index', compact('threads'));
    }
index.blade.php
        {{-- 投稿 --}}
+       @foreach ($threads as $thread)
            <div class="bg-white rounded-md mt-1 mb-5 p-3">
                {{-- スレッド --}}
                <div>
-                   <p class="mb-2 text-xs">2021/11/20 18:00 @Noname</p>
+                   <p class="mb-2 text-xs">{{$thread->created_at}}{{$thread->user_name}}</p>
-                   <p class="mb-2 text-xl font-bold">●●について</p>
+                   <p class="mb-2 text-xl font-bold">{{$thread->message_title}}</p>
-                   <p class="mb-2">これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。これは本文です。</p>
+                   <p class="mb-2">{{$thread->message}}</p>
                </div>
                {{-- 削除ボタン --}}
                <form class="flex justify-end mt-5" action="/" method="POST">
                    @csrf
                    <input class="border rounded px-2 flex-auto" type="text" name="reply_message">
                    <input class="px-2 py-1 ml-2 rounded bg-green-600 text-white font-bold link-hover cursor-pointer" type="submit" value="返信">
                    <input class="px-2 py-1 ml-2 rounded bg-red-500 text-white font-bold link-hover cursor-pointer" type="submit" value="削除">
                </form>
                {{-- 返信 --}}
                <hr class="mt-2 m-auto">
                <div class="flex justify-end">
                    <div class="w-11/12">
                        <div>
                            <p class="mt-2 text-xs">2021/11/20 19:00 @Noname</p>
                            <p class="my-2 text-sm">これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。</p>
                        </div>
                    </div>
                </div>
            </div>
+       @endforeach

ここまでで、http://localhost:8000へアクセスすると、下記画面が表示されます。

DaiNakaDaiNaka

データベースのリレーションと表示(リプライ)

ここまででは、リプライ情報はベタ打ちのテキストを表示させていましたが、データベースから値を取得して表示する様にしていきます。
また、ここではスレッドとリプライのテーブルをリレーションさせていきます。

補足

  • データベースにテストデータを登録済みです。

スレッドのモデルに下記の情報を追記する事で、リプライ(子テーブル)にあるthread_idを基に、スレッド(親テーブル)に紐づくリプライ情報を引っ張ってくる事ができます。

Thread.php
class Thread extends Model
{
    use HasFactory;
+   public function replies()
+   {
+       return $this->hasMany(Reply::class);
+   }
}

詳しくは、ReaDoubleさんのページをご参考ください。
https://readouble.com/laravel/8.x/ja/eloquent-relationships.html#one-to-many

index.blade.php
                {{-- 返信 --}}
                <hr class="mt-2 m-auto">
                <div class="flex justify-end">
                    <div class="w-11/12">
+                       @foreach ($thread->replies as $reply)
                            <div>
-                               <p class="mt-2 text-xs">2021/11/20 19:00 @Noname</p>
+                               <p class="mt-2 text-xs">{{$reply->created_at}}{{$reply->user_name}}</p>
-                               <p class="my-2 text-sm">これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。これは返信です。</p>
+                               <p class="my-2 text-sm">{{$reply->message}}</p>
                            </div>
                        @endforeach
                    </div>
                </div>

ここまでで、http://localhost:8000へアクセスすると、下記画面が表示されます。

DaiNakaDaiNaka

スレッドの登録処理

ここでは、スレッドの投稿をした時の処理を書いていきたいと思います。
まずはindex.blade.phpへ投稿した時の処理を編集していきます。

index.blade.php
        {{-- 入力フォーム --}}
        <div class="bg-white rounded-md mt-5 p-3">
-           <form action="/" method="POST">
+           <form action="{{route('bbs.store')}}" method="POST">
                @csrf
                <div class="flex">
                    <p class="font-bold">名前</p>
-                     <input class="border rounded px-2 ml-2" type="text" name="user_name">
+                 <input class="border rounded px-2 ml-2" type="text" name="user_name" required>
                </div>
                <div class="flex mt-2">
                    <p class="font-bold">件名</p>
-                   <input class="border rounded px-2 ml-2 flex-auto" type="text" name="message_title">
+                   <input class="border rounded px-2 ml-2 flex-auto" type="text" name="message_title" required>
                </div>
                <div class="flex flex-col mt-2">
                    <p class="font-bold">本文</p>
-                   <textarea class="border rounded px-2" name="message"></textarea>
+                   <textarea class="border rounded px-2" name="message" required></textarea>
                </div>
                <div class="flex justify-end mt-2">
                    <input class="my-2 px-2 py-1 rounded bg-blue-300 text-blue-900 font-bold link-hover cursor-pointer" type="submit" value="投稿">
                </div>
            </form>
        </div>

変更点の説明

  • 投稿ボタンが押された時にポストする先を変更しています
  • それぞれのフォームについて、必須入力にしています

次にThread.phpモデルに対しておまじないを書いていきます。
※今はおまじないの様な感じで覚えていますが、今後は深堀りしてしっかりと理解したいと思っています。

Thread.php
class Thread extends Model
{
    use HasFactory;
+   protected $fillable = ['user_name','message_title','message'];
    public function replies()
    {
        return $this->hasMany(Reply::class);
    }
}

変更点の説明

  • ThreadController.phpでフォームの値を扱う際にエラーが起きない様にしています

最後にThreadController.phpに対してフォームへ入力されたスレッド情報をデータベースへ登録していく処理を書いていきます。

ThreadController.php
    public function store(Request $request)
    {
-       //
+       // フォームに入力されたスレッド情報をデータベースへ登録
+       $threads = new Thread;
+       $form = $request->all();
+       $threads->fill($form)->save();
+       return redirect('/');
    }

画面では分からないので画面キャプチャは省略しますが、ここまでの処理でスレッドを投稿すると反映される様になります。

最新情報から上位に表示される様に修正

現在のところ、最新の情報が下位に表示される様になっている為、ThreadController.phpを修正していきます。

ThreadController.php
-       // スレッド情報を取得して代入
+       // スレッド情報を取得して代入(最新情報を上位に表示)
-       $threads = Thread::all();
+       $threads = Thread::orderBy('created_at', 'desc')->get();

スレッド情報をデータベースに登録されている通りに全て取得していたものを、created_atの降順(desc)に並べ替えて取得しています。

DaiNakaDaiNaka

リプライの登録処理のコーディング

ここでは、リプライの投稿をした時の処理を書いていきたいと思います。
まずはindex.blade.phpへ投稿した時の処理を編集していきます。

index.blade.php
                {{-- 削除ボタン --}}
-               <form class="flex justify-end mt-5" action="/" method="POST">
+               <form class="flex justify-end mt-5" action="{{route('reply.store')}}" method="POST">
                    @csrf
+                   <input type="hidden" name="thread_id" value={{$thread->id}}>
+                   <input class="border rounded px-2 flex-initial" type="text" name="user_name" placeholder="UserName" required>
-                   <input class="border rounded px-2 flex-auto" type="text" name="reply_message">
+                   <input class="border rounded px-2 ml-2 flex-auto" type="text" name="message" placeholder="ReplyMessage" required>
                    <input class="px-2 py-1 ml-2 rounded bg-green-600 text-white font-bold link-hover cursor-pointer" type="submit" value="返信">
-                   <input class="px-2 py-1 ml-2 rounded bg-red-500 text-white font-bold link-hover cursor-pointer" type="submit" value="削除">
+                   <input class="px-2 py-1 ml-2 rounded bg-red-500 text-white font-bold link-hover cursor-pointer" type="button" value="削除">
                </form>

変更点の説明

  • 投稿ボタンが押された時にポストする先を変更しています
  • ThreadController.phpで処理をする際に必要なので、隠し要素(hidden)でthread_idを格納しています
  • それぞれのフォームについて、必須入力にしています
  • 削除ボタンが押された時にポストされない様にしています
  • 一部は修正を加えていたりします(reply_message -> message)

次にポストされた処理のルーティングをweb.phpへ追記しています。

web.php
+ use App\Http\Controllers\ReplyController;
+ Route::resource('/reply', ReplyController::class);

スレッド投稿の時の処理と同様ですが、Reply.phpモデルに対しておまじないを書いていきます。
※今はおまじないの様な感じで覚えていますが、今後は深堀りしてしっかりと理解したいと思っています。

Reply.php
class Reply extends Model
{
    use HasFactory;
+   protected $fillable = ['thread_id', 'user_name', 'message'];
}

変更点の説明

  • ReplyController.phpでフォームの値を扱う際にエラーが起きない様にしています

最後にReplyController.phpに対してフォームへ入力されたリプライ情報をデータベースへ登録していく処理を書いていきます。

ReplyController.php
    public function store(Request $request)
    {
-       //
+       // フォームに入力されたリプライ情報をデータベースへ登録
+       $replies = new Reply;
+       $form = $request->all();
+       $replies->fill($form)->save();
+       return redirect('/');
    }

画面では分からないので画面キャプチャは省略しますが、ここまでの処理でリプライを投稿すると反映される様になります。

DaiNakaDaiNaka

スレッドの削除処理

ここではスレッド情報を削除する時の処理を書いていきます。
まずはindex.blade.phpを編集します。

index.blade.php
-               {{-- 削除ボタン --}}
+               {{-- ボタン --}}
+               <div class="flex mt-5">
+                   {{-- 返信 --}}
-                   <form class="flex justify-end mt-5" action="{{route('reply.store')}}" method="POST">
+                   <form class="flex justify-end flex-auto" action="{{route('reply.store')}}" method="POST">
                        @csrf
                        <input type="hidden" name="thread_id" value={{$thread->id}}>
                        <input class="border rounded px-2 flex-initial" type="text" name="user_name" placeholder="UserName" required>
                        <input class="border rounded px-2 ml-2 flex-auto" type="text" name="message" placeholder="ReplyMessage" required>
                        <input class="px-2 py-1 ml-2 rounded bg-green-600 text-white font-bold link-hover cursor-pointer" type="submit" value="返信">
-                   <input class="px-2 py-1 ml-2 rounded bg-red-500 text-white font-bold link-hover cursor-pointer" type="button" value="削除">
                    </form>
+                   {{-- 削除 --}}
+                   <form action="{{route('thread.destroy', ['thread'=>$thread->id])}}" method="post">
+                       @csrf
+                       @method('DELETE')
+                       <input class="px-2 py-1 ml-2 rounded bg-red-500 text-white font-bold link-hover cursor-pointer" type="submit" value="削除" onclick="return Check()">
+                   </form>
+               </div>


+   {{-- スレッド削除の確認 --}}
+   <script type="text/javascript">
+       function Check(){
+           var checked = confirm("本当に削除しますか?");
+           if (checked == true) { return true; } else { return false; }
+       }
+   </script>

変更点の説明

  • 削除用のフォームを作成して返信用のフォームから分離
  • 削除ボタンをクリックした時に確認メッセージが表示される様に処理
  • 一部のCSSを調整

削除の確認処理(javascript)について

javascriptはまだ勉強中の為、下記のページを参考にしました。
今回はほぼ丸コピーですが、今後は習得していきたいと思います。
https://qiita.com/Rys8/items/aad482a4bc3bf823c188

次にThreadController.phpを編集します。

ThreadController.php
    public function destroy($id)
    {
-       //
+       // スレッド情報をデータベースから削除
+       $thread = Thread::find($id)->delete();
+       return redirect('/');
    }

ここまでの処理を書いて削除ボタンをクリックすると、下記の画面の様に確認画面が表示される様になりした。
OKをクリックすると、スレッド情報が削除される様になっています。
今回は、特に何も考えずに論理削除しています。

ソフトデリートと論理削除について簡単に解説してみます。

ソフトデリート

  • データに削除フラグを付けて、フラグの有無で削除されたデータかを判別する
  • データベースにはデータとして残っている
  • 復旧する事ができる

論理削除

  • データベース上からデータ自体を削除する
  • 復旧する事が出来ない
DaiNakaDaiNaka

一部ルーティング情報を修正

web.php
- Route::redirect('/', '/bbs');
+ Route::redirect('/', '/thread');
- Route::resource('/bbs', ThreadController::class);
+ Route::resource('/thread', ThreadController::class);
index.blade.php
        {{-- 入力フォーム --}}
        <div class="bg-white rounded-md mt-5 p-3">
-           <form action="{{route('bbs.store')}}" method="POST">
+             <form action="{{route('thread.store')}}" method="POST">
                @csrf
                <div class="flex">
                    <p class="font-bold">名前</p>
                    <input class="border rounded px-2 ml-2" type="text" name="user_name" required>
                </div>
                <div class="flex mt-2">
                    <p class="font-bold">件名</p>
                    <input class="border rounded px-2 ml-2 flex-auto" type="text" name="message_title" required>
                </div>
                <div class="flex flex-col mt-2">
                    <p class="font-bold">本文</p>
                    <textarea class="border rounded px-2" name="message" required></textarea>
                </div>
                <div class="flex justify-end mt-2">
                    <input class="my-2 px-2 py-1 rounded bg-blue-300 text-blue-900 font-bold link-hover cursor-pointer" type="submit" value="投稿">
                </div>
            </form>
        </div>
DaiNakaDaiNaka

ページネーションの処理

ここではスレッドが多数ある時にページ分けをする処理を書いていきます。
まずはThreadController.phpを編集します。

ThreadController.php
    public function index()
    {
        // スレッド情報を取得して代入(最新情報を上位に表示)
-       $threads = Thread::orderBy('created_at', 'desc')->get();
+       $threads = Thread::orderBy('created_at', 'desc')->Paginate(5);

        // 掲示板ページを表示
        return view('bbs/index', compact('threads'));
    }

変更点の説明

  • データベースから5件ずつ取得される様に変更

次にindex.blade.phpを編集します。

index.blade.php
        {{-- ページネーション --}}
-       <p class="flex justify-center text-blue-300 mt-5 link-hover cursor-pointer">prev 1 2 3 4 next</p>
+       <p class="mt-5">{{ $threads->links() }}</p>

変更点の説明

  • ベタ打ちテキストをページネーションのコードに変更

ここまでで、http://localhost:8000へアクセスすると、下記画面が表示されます。

DaiNakaDaiNaka

検索フォームの処理

ここでは検索フォームで検索した時の処理を書いていきます。
まだ私には早すぎた様で、ちょっと複雑で分かるのに少し時間が掛かりました。
※まだ理解したとは言い難い・・・

そこで、参考にしたサイトを紹介しつつ、躓いたポイントを含めて書いていきたいと思います。

自身で思った通りに進めてみた過程

まずはindex.blade.phpを編集していきます。

index.blade.php
        {{-- 検索フォーム --}}
        <div class="bg-white rounded-md mt-3 p-3">
-           <form action="/" method="post">
+           <form action="{{route('thread.search')}}" method="post">
                @csrf
                <div class="mx-1 flex">
-                   <input class="border rounded px-2 flex-auto" type="text" name="serch_message">
+                   <input class="border rounded px-2 flex-auto" type="text" name="search_message" required>
                    <input class="ml-2 px-2 py-1 rounded bg-gray-500 text-white font-bold link-hover cursor-pointer" type="submit" value="検索">
                </div>
            </form>
        </div>

変更点の説明

  • 検索ボタンを押した時にポストする先を変更
  • 検索メッセージを必須に変更
  • 一部は修正を加えていたりします(serch -> search)

次にweb.phpを変更して検索した時のルーティング情報を追記

web.php
+ Route::post('/thread/search', [ThreadController::class, 'search'])->name('thread.search');

最後にThreadController.phpを編集していきます。

ThreadController.php
+    public function search(Request $request)
+    {
+        // 検索フォームに入力された単語でLIKE検索した結果のスレッド情報を取得して代入(最新情報を上位に表示)
+        $threads = Thread::where('message', '*' . $request->search_message . '*')->orderBy('created_at', 'desc')->Paginate(5);
+
+        // 掲示板ページを表示
+        return view('bbs/index', compact('threads'));
+    }

ここまでで、http://localhost:8000へアクセスして、検索してみた結果、下記画面が表示されます。

何も検索にヒットしていない・・・

間違った部分の解説

  • ワイルドカードが*だと思い込んでいたが、正解は%である
  • where句にLIKEが入っていない

そりゃ検索にヒットしない訳だわ・・・
そこで、下記のページを参考にしながら実装していきます。
https://qiita.com/take_3/items/1154ecbd8033a9a3beaf

検索ができる様に修正

ThreadController.php
     public function search(Request $request)
     {
+       // 検索フォームに入力された単語のエスケープ処理
+       $search_message = '%' . addcslashes($request->search_message, '%_\\') . '%';

         // 検索フォームに入力された単語でLIKE検索した結果のスレッド情報を取得して代入(最新情報を上位に表示)
-        $threads = Thread::where('message', '*' . $request->search_message . '*')->orderBy('created_at', 'desc')->Paginate(5);
         $threads = Thread::where('message', 'LIKE', $search_message)->orderBy('created_at', 'desc')->Paginate(5);
 
         // 掲示板ページを表示
         return view('bbs/index', compact('threads'));
     }

変更点の説明

  • addcslashesを使って、検索フォームに入力された単語の指定文字の前に\を付与
  • また入力された文字の前後に%(ワイルドカード)を付与
  • 上記の結果を変数に代入してLIKE検索

addcslashesに関する詳細はPHP公式のページが分かり易かったです。
https://www.php.net/manual/ja/function.addcslashes.php

ここまでで、http://localhost:8000へアクセスして、検索してみた結果、無事に検索結果が反映されて表示されました。

DaiNakaDaiNaka

モバイル表示した際のバグフィックス

実はモバイル表示すると下記の様に返信フォームが飛び出していました。

そこでindex.blade.phpを編集してCSSを調整していきます。

index.blade.php
                    {{-- 返信 --}}
-                   <form class="flex justify-end flex-auto" action="{{route('reply.store')}}" method="POST">
+                   <form class="flex flex-auto" action="{{route('reply.store')}}" method="POST">
                        @csrf
                        <input type="hidden" name="thread_id" value={{$thread->id}}>
-                       <input class="border rounded px-2 flex-initial" type="text" name="user_name" placeholder="UserName" required>
+                       <input class="border rounded px-2 w-2/5 md:w-4/12 text-sm md:text-base" type="text" name="user_name" placeholder="UserName" required>
-                       <input class="border rounded px-2 ml-2 flex-auto" type="text" name="message" placeholder="ReplyMessage" required>
+                       <input class="border rounded px-2 ml-2 w-3/5 md:w-10/12 text-sm md:text-base" type="text" name="message" placeholder="ReplyMessage" required>
                        <input class="px-2 py-1 ml-2 rounded bg-green-600 text-white font-bold link-hover cursor-pointer" type="submit" value="返信">
                    </form>

これで無事にモバイル表示しても検索フォームが飛び出さない様になりました。

DaiNakaDaiNaka

スレッドの削除処理の修正

ここまででスレッドの削除は出来る様になっていますが、他人が投稿したスレッドも削除出来てしまいます。
そこで自身が投稿したスレッドしか削除が出来ない様に修正していきたいと思います。
まずは、Threadsテーブルにユーザー識別子を追加していきます。

カラム追加用マイグレーションファイルの生成

下記のコマンドを実行して、カラムの追加用マイグレーションファイルを生成します。
コマンド:php artisan make:migration add_user_identifier_to_threads_table
生成場所:database\migrations\yyyy_mm_dd_hhmmss_add_user_identifier_to_threads_table.php

yyyy_mm_dd_hhmmss_add_user_identifier_to_threads_table.php
<?php

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

class AddUserIdentifierToThreadsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('threads', function (Blueprint $table) {
            //
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('threads', function (Blueprint $table) {
            //
        });
    }
}

次にカラムの追加をしていきます。

yyyy_mm_dd_hhmmss_add_user_identifier_to_threads_table.php
    public function up()
    {
        Schema::table('threads', function (Blueprint $table) {
-           //
+           $table->string('user_identifier', 20)->after('user_name');
        });
    }

user_nameカラムの後方にuser_identifierカラムを追加する様に設定しています。

マイグレーション

それではカラムに追加する為、マイグレーションしていきます。
コマンド:php artisan migrate
下の画面はマイグレーション後のデータベースとなります。

ユーザー識別子の生成

ここからはセッションにユーザー名とユーザー識別子を登録して利用者の利便性も含めて編集していきたいと思います。

ThreadController.php
-    public function index()
+    public function index(Request $request)
    {
+       // ユーザー識別子をセッションに登録(なければランダムに生成)
+       if($request->session()->missing('user_identifier')){ session(['user_identifier' => Str::random(20)]); }

+       // ユーザー名をセッションに登録(なければGuestとして登録)
+       if($request->session()->missing('user_name')){ session(['user_name' => 'Guest']); }

        // スレッド情報を取得して代入(最新情報を上位に表示)
        $threads = Thread::orderBy('created_at', 'desc')->Paginate(5);

        // 掲示板ページを表示
        return view('bbs/index', compact('threads'));
    }
ThreadController.php
    public function store(Request $request)
    {
+       // フォームで入力されたユーザー名をセッションに登録
+       session(['user_name' => $request->user_name]);

        // フォームに入力されたスレッド情報をデータベースへ登録
        $threads = new Thread;
        $form = $request->all();
        $threads->fill($form)->save();
        return redirect('/');
    }

次にindex.blade.phpを編集していきます。

index.blade.php
        {{-- 入力フォーム --}}
        <div class="bg-white rounded-md mt-5 p-3">
            <form action="{{route('thread.store')}}" method="POST">
                @csrf
+               <input type="hidden" name="user_identifier" value="{{session('user_identifier')}}">
                <div class="flex">
                    <p class="font-bold">名前</p>
-                   <input class="border rounded px-2 ml-2" type="text" name="user_name" required>
+                   <input class="border rounded px-2 ml-2" type="text" name="user_name" value="{{session('user_name')}}" required>
                </div>
                <div class="flex mt-2">
                    <p class="font-bold">件名</p>
-                   <input class="border rounded px-2 ml-2 flex-auto" type="text" name="message_title" required>
+                   <input class="border rounded px-2 ml-2 flex-auto" type="text" name="message_title" required autofocus>
                </div>
                <div class="flex flex-col mt-2">
                    <p class="font-bold">本文</p>
                    <textarea class="border rounded px-2" name="message" required></textarea>
                </div>
                <div class="flex justify-end mt-2">
                    <input class="my-2 px-2 py-1 rounded bg-blue-300 text-blue-900 font-bold link-hover cursor-pointer" type="submit" value="投稿">
                </div>
            </form>
        </div>

変更点の説明

  • 隠し要素でフォームにユーザー識別子を設置
  • ユーザー名を事前にフォームへ登録
  • 件名にオートフォーカス
index.blade.php
                {{-- ボタン --}}
                <div class="flex mt-5">
                    {{-- 返信 --}}
                    <form class="flex flex-auto" action="{{route('reply.store')}}" method="POST">
                        @csrf
                        <input type="hidden" name="thread_id" value={{$thread->id}}>
-                       <input class="border rounded px-2 w-2/5 md:w-4/12 text-sm md:text-base" type="text" name="user_name" placeholder="UserName" required>
+                       <input class="border rounded px-2 w-2/5 md:w-4/12 text-sm md:text-base" type="text" name="user_name" placeholder="UserName" value="{{session('user_name')}}" required>
                        <input class="border rounded px-2 ml-2 w-3/5 md:w-10/12 text-sm md:text-base" type="text" name="message" placeholder="ReplyMessage" required>
                        <input class="px-2 py-1 ml-2 rounded bg-green-600 text-white font-bold link-hover cursor-pointer" type="submit" value="返信">
                    </form>
                    {{-- 削除 --}}
+                   @if ($thread->user_identifier == session('user_identifier'))
                        <form action="{{route('thread.destroy', ['thread'=>$thread->id])}}" method="post">
                            @csrf
                            @method('DELETE')
                            <input class="px-2 py-1 ml-2 rounded bg-red-500 text-white font-bold link-hover cursor-pointer" type="submit" value="削除" onclick="return Check()">
                        </form>
+                   @endif
                </div>

変更点の説明

  • ユーザー名を事前にフォームへ登録
  • ユーザー識別子が一致した時だけ削除ボタンを設置(投稿者にしか表示されない)

ここまでで、http://localhost:8000へアクセスすると、下記画面が表示されます。

DaiNakaDaiNaka

おわりに

第二弾的に始めて約5日間程で完成しました。
実際には、参考にしたページの解説を見ないと実装出来なかったりと、まだまだな部分はありますが、以前よりも大分レベルが上がってきている気がしています。

また、練習用ではなく、世の中に出す様なシステムであれば、もっとコードの見直しやテスト等を繰り返して、より利便性を向上させたりといった事が必要かと思います。

いずれは、自身の力で企画からコーディングまで実施して、世に出していける様になったら人生楽しいだろうとワクワクしています。

リファクタリングして不要な部分を大幅に削ぎ落したコードを下記に置いてありますので、気になる方はご覧ください。

それではまた次回。

https://github.com/DaiNaka1207/bbs-app

DaiNakaDaiNaka

デモサイト

作成したデモサイトは下記にデプロイしてあります。
ご自由に書き込みをテストしてください。
なお、テスト書き込みが多くなってきた為、一度データをクリアしています。

https://bbs-app.dainaka.live/thread