【挑戦】Laravel 8 で簡易的な掲示板を作ってみた
はじめに
シリーズ化ではないですが、開発の手を止める事なくインプット&アウトプットを繰り返していく事で自身の技術力アップを図っていく事を目的に挑戦していく過程を収めていきたいと思います。
折角なので、アウトプットについても分かりやすくを意識していこうと思います。
前回の挑戦
開発環境
- XAMPP v3.3.0
- composer 2.1.3
- VS Code
利用言語
- PHP
- Laravel 8
- tailwind
公開コード[GitHub]
企画
昔ながらのCGIで作成された様な簡易的な掲示板を作っていこうと思います。
完成イメージの参考サイト
機能
- ログインする必要がなく、直ぐに投稿する事ができる
- 投稿した本人であればスレッドを削除する事ができる
- 投稿されたスレッドに対して返信する事ができる
- 検索をする事ができる
プロジェクトの作成
まずは、コマンドプロンプトにて、下記のコードを実行してプロジェクトの作成から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
環境設定
- APP_NAME=laravel
+ APP_NAME=bbs-app
- 'timezone' => 'UTC',
+ 'timezone' => 'Asia/Tokyo',
- 'locale' => 'en',
+ 'locale' => 'ja',
- 'faker_locale' => 'en_US',
+ 'faker_locale' => 'ja_JP',
最後に、.env
ファイル内のDB_DATABASE=bbs_app
を参考にデータベースを作成します。
Tailwindcssのインストール
今回も、前回同様にTailwindcssを使っていきたいと思いますので、SEの休日さんのページを参考にインストールしていきます。
この環境設定のところは、理解して自分自身で設定ができる様になると良いと思いますが、今回の趣旨からは外れてしまう為、深堀りはしないつもりです。
ただ、いつかこういった設定も自身でできる様になり、説明もできる様になりたいと思っています。
掲示板ページのビュー作成とルーティング設定
掲示板ページのビューを作成してルーティング設定するまでを書いていこうと思います。
ビューの作成
作成場所:resources\views\bbs\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ページは削除して、サイトトップページを開こうとすると掲示板のトップページにリダイレクトしています。
- Route::get('/', function () {
- return view('welcome');
- });
+ Route::redirect('/', '/bbs');
+ Route::view('/bbs', '/bbs/index');
ここまでで、http://localhost:8000
へアクセスすると、下記画面が表示されます。
掲示板ページの画面構成をコーディング
ここではフロントエンド部分コーディングをしていこうと思います。
まずは、ベタ打ちのテキストやフォームをコーディングしていきます。
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
へアクセスすると、下記画面が表示されます。
データベースの構成を検討
[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 |
テーブルの作成
データベースの構成を考えたら、マイグレーションファイルを作成してテーブルの作成を実行していきます。
[threads]テーブルの作成
コマンド:php artisan make:migration create_threads_table
生成場所:database\migrations\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
<?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');
}
}
カラムの設定
それぞれのファイルを更新して、カラムの設定をしていきます。
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();
});
}
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]テーブル
コントローラーの作成
ここでは、データベースを扱うコントローラーを作成していきます。
コマンド:php artisan make:controller ThreadController --resource
生成場所:app\Http\Controllers\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
<?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)
{
//
}
}
コントローラーを経由したビュー表示へ変更
ここまででは、ルーティングでビューを表示する様に設定していましたが、コントローラーを経由したビュー表示へ切り替えていきます。
public function index()
{
- //
+ // 掲示板ページを表示
+ return view('bbs/index');
}
+ use App\Http\Controllers\ThreadController;
- Route::view('/bbs', '/bbs/index');
+ Route::resource('/bbs', ThreadController::class);
データベースとの連携と表示(スレッド)
ここまででは、ベタ打ちのテキストを表示させていましたが、データベースから値を取得して表示する様にしていきます。
補足
- データベースにテストデータを登録済みです。
- リプライ情報は、まだベタ打ちのテキストを表示しています。
public function index()
{
// スレッド情報を取得して代入
+ $threads = Thread::all();
// 掲示板ページを表示
- return view('bbs/index');
+ return view('bbs/index', compact('threads'));
}
{{-- 投稿 --}}
+ @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
へアクセスすると、下記画面が表示されます。
データベースのリレーションと表示(リプライ)
ここまででは、リプライ情報はベタ打ちのテキストを表示させていましたが、データベースから値を取得して表示する様にしていきます。
また、ここではスレッドとリプライのテーブルをリレーションさせていきます。
補足
- データベースにテストデータを登録済みです。
スレッドのモデルに下記の情報を追記する事で、リプライ(子テーブル)にあるthread_id
を基に、スレッド(親テーブル)に紐づくリプライ情報を引っ張ってくる事ができます。
class Thread extends Model
{
use HasFactory;
+ public function replies()
+ {
+ return $this->hasMany(Reply::class);
+ }
}
詳しくは、ReaDoubleさんのページをご参考ください。
{{-- 返信 --}}
<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
へアクセスすると、下記画面が表示されます。
スレッドの登録処理
ここでは、スレッドの投稿をした時の処理を書いていきたいと思います。
まずは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
モデルに対しておまじないを書いていきます。
※今はおまじないの様な感じで覚えていますが、今後は深堀りしてしっかりと理解したいと思っています。
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
に対してフォームへ入力されたスレッド情報をデータベースへ登録していく処理を書いていきます。
public function store(Request $request)
{
- //
+ // フォームに入力されたスレッド情報をデータベースへ登録
+ $threads = new Thread;
+ $form = $request->all();
+ $threads->fill($form)->save();
+ return redirect('/');
}
画面では分からないので画面キャプチャは省略しますが、ここまでの処理でスレッドを投稿すると反映される様になります。
最新情報から上位に表示される様に修正
現在のところ、最新の情報が下位に表示される様になっている為、ThreadController.php
を修正していきます。
- // スレッド情報を取得して代入
+ // スレッド情報を取得して代入(最新情報を上位に表示)
- $threads = Thread::all();
+ $threads = Thread::orderBy('created_at', 'desc')->get();
スレッド情報をデータベースに登録されている通りに全て取得していたものを、created_at
の降順(desc)に並べ替えて取得しています。
リプライの登録処理のコーディング
ここでは、リプライの投稿をした時の処理を書いていきたいと思います。
まずは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
へ追記しています。
+ use App\Http\Controllers\ReplyController;
+ Route::resource('/reply', ReplyController::class);
スレッド投稿の時の処理と同様ですが、Reply.php
モデルに対しておまじないを書いていきます。
※今はおまじないの様な感じで覚えていますが、今後は深堀りしてしっかりと理解したいと思っています。
class Reply extends Model
{
use HasFactory;
+ protected $fillable = ['thread_id', 'user_name', 'message'];
}
変更点の説明
-
ReplyController.php
でフォームの値を扱う際にエラーが起きない様にしています
最後にReplyController.php
に対してフォームへ入力されたリプライ情報をデータベースへ登録していく処理を書いていきます。
public function store(Request $request)
{
- //
+ // フォームに入力されたリプライ情報をデータベースへ登録
+ $replies = new Reply;
+ $form = $request->all();
+ $replies->fill($form)->save();
+ return redirect('/');
}
画面では分からないので画面キャプチャは省略しますが、ここまでの処理でリプライを投稿すると反映される様になります。
スレッドの削除処理
ここではスレッド情報を削除する時の処理を書いていきます。
まずは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はまだ勉強中の為、下記のページを参考にしました。
今回はほぼ丸コピーですが、今後は習得していきたいと思います。
次にThreadController.php
を編集します。
public function destroy($id)
{
- //
+ // スレッド情報をデータベースから削除
+ $thread = Thread::find($id)->delete();
+ return redirect('/');
}
ここまでの処理を書いて削除ボタンをクリックすると、下記の画面の様に確認画面が表示される様になりした。
OK
をクリックすると、スレッド情報が削除される様になっています。
今回は、特に何も考えずに論理削除しています。
ソフトデリートと論理削除について簡単に解説してみます。
ソフトデリート
- データに削除フラグを付けて、フラグの有無で削除されたデータかを判別する
- データベースにはデータとして残っている
- 復旧する事ができる
論理削除
- データベース上からデータ自体を削除する
- 復旧する事が出来ない
一部ルーティング情報を修正
- Route::redirect('/', '/bbs');
+ Route::redirect('/', '/thread');
- Route::resource('/bbs', ThreadController::class);
+ Route::resource('/thread', ThreadController::class);
{{-- 入力フォーム --}}
<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>
ページネーションの処理
ここではスレッドが多数ある時にページ分けをする処理を書いていきます。
まずは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
を編集します。
{{-- ページネーション --}}
- <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
へアクセスすると、下記画面が表示されます。
検索フォームの処理
ここでは検索フォームで検索した時の処理を書いていきます。
まだ私には早すぎた様で、ちょっと複雑で分かるのに少し時間が掛かりました。
※まだ理解したとは言い難い・・・
そこで、参考にしたサイトを紹介しつつ、躓いたポイントを含めて書いていきたいと思います。
自身で思った通りに進めてみた過程
まずは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
を変更して検索した時のルーティング情報を追記
+ Route::post('/thread/search', [ThreadController::class, 'search'])->name('thread.search');
最後に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
が入っていない
そりゃ検索にヒットしない訳だわ・・・
そこで、下記のページを参考にしながら実装していきます。
検索ができる様に修正
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公式のページが分かり易かったです。
ここまでで、http://localhost:8000
へアクセスして、検索してみた結果、無事に検索結果が反映されて表示されました。
モバイル表示した際のバグフィックス
実はモバイル表示すると下記の様に返信フォームが飛び出していました。
そこでindex.blade.php
を編集してCSSを調整していきます。
{{-- 返信 --}}
- <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>
これで無事にモバイル表示しても検索フォームが飛び出さない様になりました。
スレッドの削除処理の修正
ここまででスレッドの削除は出来る様になっていますが、他人が投稿したスレッドも削除出来てしまいます。
そこで自身が投稿したスレッドしか削除が出来ない様に修正していきたいと思います。
まずは、Threads
テーブルにユーザー識別子を追加していきます。
カラム追加用マイグレーションファイルの生成
下記のコマンドを実行して、カラムの追加用マイグレーションファイルを生成します。
コマンド:php artisan make:migration add_user_identifier_to_threads_table
生成場所:database\migrations\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) {
//
});
}
}
次にカラムの追加をしていきます。
public function up()
{
Schema::table('threads', function (Blueprint $table) {
- //
+ $table->string('user_identifier', 20)->after('user_name');
});
}
user_name
カラムの後方にuser_identifier
カラムを追加する様に設定しています。
マイグレーション
それではカラムに追加する為、マイグレーションしていきます。
コマンド:php artisan migrate
下の画面はマイグレーション後のデータベースとなります。
ユーザー識別子の生成
ここからはセッションにユーザー名とユーザー識別子を登録して利用者の利便性も含めて編集していきたいと思います。
- 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'));
}
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
を編集していきます。
{{-- 入力フォーム --}}
<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>
変更点の説明
- 隠し要素でフォームにユーザー識別子を設置
- ユーザー名を事前にフォームへ登録
- 件名にオートフォーカス
{{-- ボタン --}}
<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
へアクセスすると、下記画面が表示されます。
おわりに
第二弾的に始めて約5日間程で完成しました。
実際には、参考にしたページの解説を見ないと実装出来なかったりと、まだまだな部分はありますが、以前よりも大分レベルが上がってきている気がしています。
また、練習用ではなく、世の中に出す様なシステムであれば、もっとコードの見直しやテスト等を繰り返して、より利便性を向上させたりといった事が必要かと思います。
いずれは、自身の力で企画からコーディングまで実施して、世に出していける様になったら人生楽しいだろうとワクワクしています。
リファクタリングして不要な部分を大幅に削ぎ落したコードを下記に置いてありますので、気になる方はご覧ください。
それではまた次回。
CacheとSessionとCookieについて
別記事にしてアップしました。
デモサイト
作成したデモサイトは下記にデプロイしてあります。
ご自由に書き込みをテストしてください。
なお、テスト書き込みが多くなってきた為、一度データをクリアしています。