Laravel で簡単な掲示板を作る
Laravel Breeze を使って簡単な掲示板を作ります。
- スレッド・コメントの閲覧は誰でも可能
- スレッド・コメントの作成はログインユーザーのみ
というような要件にします。
この記事で扱うこと
- 認証
- スレッド作成・閲覧
- コメント作成・閲覧・削除
Laravel Breeze とは
Laravel のスターターキットには Jetstream と Breeze があります。Jetstream が非常に高機能で、よりシンプルな認証機能を提供するのが Breeze です。Laravel の初心者は Breeze で要領を覚えてから Jetstream に進むことが推奨されています。
For those brand new to Laravel, we recommend learning the ropes with Laravel Breeze before graduating to Laravel Jetstream.
環境構築
Windows に PHP 環境構築(XAMPP, Composer)
以前記事にしました。OS は Windows ですが、XAMPP を使用しているので他の OS でも似たような手順になると思います。
プロジェクト作成
composer create-project laravel/laravel laravel-bbs-app
プロジェクトが作成されたら、ディレクトリに移動しましょう。
cd laravel-bbs-app
Apache と MySQL の起動・DB 設定
データベースの設定をします。ここで設定をしておかないと、Breeze 導入の php artisan migrate
でエラーになります。まずは XAMPP のコントロールパネルで Apache と MySQL を "Start" させます。
MySQL の Admin をクリックし、Admin パネルを開きましょう。新しいデータベースを作成します。
そして、Laravel プロジェクトの .env
ファイルの DB_DATABASE を変更します。
// .env
- DB_DATABASE=laravel
+ DB_DATABASE=laravel_bbs_test
これで設定は完了です。
Breeze (+ Tailwind CSS) 導入
Laravel Breeze をインストールすると、勝手に Tailwind CSS が付いてきます。Breeze はログイン画面やユーザー登録画面、ナビゲーションなどのシンプルな UI を提供してくれます。そのスタイリングに Tailwind CSS が使用されています。
composer require laravel/breeze --dev
インストールが終わったら、以下のコマンドを入力します。
php artisan breeze:install
npm install
npm run dev
php artisan migrate
これで Breeze の導入は完了です。ブラウザで見てみましょう。
php artisan serve
OS の設定がライトモードの場合は白色のデザインになっていると思います。
見づらいですが、右上に "Log in" と "Register" というリンクがあります。それぞれ Breeze がページを用意してくれています。
ログインページ
レジスターページ
Taro という名前で登録してみます。
このように、ダッシュボードページにリダイレクトされます。右上のドロップダウンからログアウトもできます。ログインした状態で進んでください。
config/app.php
の編集
config/app.php
を編集します。
- 'timezone' => 'UTC',
+ 'timezone' => 'Asia/Tokyo',
- 'locale' => 'en',
+ 'locale' => 'ja',
スレッド一覧ページの作成
ルートを追加します。
// routes/web.php
Route::get('/threads', function () {
return view('threads.index');
})->name('threads');
テンプレートを作成します。
// resources/views/threads/index.blade.php
**<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Threads') }}
</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="p-6 bg-white border-b border-gray-200">
Threads
</div>
</div>
</div>
</div>
</x-app-layout>**
dashboard.blade.php
をコピーして、少し文字を変えただけです。http://127.0.0.1:8000/threads
にアクセスすると、ちゃんと作成したページが表示されます。
ナビゲーションバーの修正
一度ログアウトして、再びスレッドページにアクセスすると、
Attempt to read property "name" on null
というエラーメッセージが表示されます。現在のレイアウトだとヘッダーの右端にユーザー名を表示しているので、表示するべきユーザーがいないとエラーになってしまいます。そこで、ナビゲーションバーを修正しましょう。
// resources/views/layouts/navigation.blade.php
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="flex-shrink-0 flex items-center">
<a href="{{ route('threads') }}">
<x-application-logo class="block h-10 w-auto fill-current text-gray-600" />
</a>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-nav-link>
</div>
</div>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ml-6">
@auth
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out">
<div>{{ Auth::user()->name }}</div>
<div class="ml-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</button>
</x-slot>
<x-slot name="content">
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-dropdown-link :href="route('logout')" onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log out') }}
</x-dropdown-link>
</form>
</x-slot>
</x-dropdown>
@endauth
@guest
<div class="flex gap-x-4">
<a href="{{ route('login') }}">{{ __('Log in') }}</a>
<a href="{{ route('register') }}">{{ __('Register') }}</a>
</div>
@endguest
</div>
<!-- Hamburger -->
<div class="-mr-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
@auth
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-responsive-nav-link>
@endauth
@guest
<x-responsive-nav-link :href="route('login')" :active="request()->routeIs('login')">
{{ __('Log in') }}
</x-responsive-nav-link>
<x-responsive-nav-link :href="route('register')" :active="request()->routeIs('register')">
{{ __('Register') }}
</x-responsive-nav-link>
@endguest
</div>
<!-- Responsive Settings Options -->
@auth
<div class="pb-1 border-t border-gray-200">
<div class="mt-2 space-y-1">
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-responsive-nav-link :href="route('logout')" onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log out') }}
</x-responsive-nav-link>
</form>
</div>
</div>
@endauth
</div>
</nav>
まず、左端のロゴのリンク先をスレッドページに変更しました。
次に、非ログイン時にはユーザー名とドロップダウンを非表示にし、代わりにログインページとレジスターページへのリンクを表示しました。また、ハンバーガーメニューを開いたときのプロフィール画像を消し、ログイン時と非ログイン時で表示するリンクを変えています。また、プロフィール画像を消した関係で、少しスタイルも修正しました。
このような画面になっていることを確認してください。これで、ログインしていないユーザーもスレッドを閲覧できます。
Thread コントローラーの作成
コントローラーを作成します。
php artisan make:controller ThreadController
スレッド一覧ページの表示をコントローラーに行わせてみましょう。
class ThreadController extends Controller
{
+ public function index()
+ {
+ return view('threads.index');
+ }
}
- Route::get('/threads', function () {
- return view('threads.index');
- })->name('threads');
+Route::get('/threads', [ThreadController::class, 'index'])->name('threads');
このように、これ以降はコントローラーを介して処理を行っていきます。
Thread 作成ページの作成
テンプレートを作成します。
<x-app-layout>
<x-slot name="header">
<div class="flex items-center justify-between">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('New Thread') }}
</h2>
</div>
</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="p-6 bg-white border-b border-gray-200">
<form action="{{ route('threads.create') }}" method="POST">
@csrf
<div>
<label for="title">{{ __('Thread title') }}</label>
<textarea name="title" id="title" cols="30" rows="2" class="w-full rounded-lg border-2 bg-gray-100 @error('title') border-red-500 @enderror"></textarea>
@error('title')
<div class="text-red-500 text-sm mt-2">
{{ $message }}
</div>
@enderror
</div>
<div class="mt-4">
<label for="body">{{ __('First comment') }}</label>
<textarea name="body" id="body" cols="30" rows="4" class="w-full rounded-lg border-2 bg-gray-100 @error('comment') border-red-500 @enderror"></textarea>
@error('body')
<div class="text-red-500 text-sm mt-2">
{{ $message }}
</div>
@enderror
</div>
<div class="mt-4">
<button type="submit" class="bg-blue-500 rounded font-medium px-4 py-2 text-white">{{ __('Submit') }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>
ルートを追加します。
// routes/web.php
+ Route::get('/threads/create', [ThreadController::class, 'create'])->name('threads.create');
+ Route::post('/threads/create', [ThreadController::class, 'create']);
コントローラーに処理を追加します。
class ThreadController extends Controller
{
public function index()
{
return view('threads.index');
}
+ public function create()
+ {
+ return view('threads.create');
+ }
+ public function store(Request $request)
+ {
+ dd([
+ $request->title,
+ $request->body,
+ ]);
+ }
}
アドレスバーから /threads/create
にアクセスしてみましょう。
このような画面が表示されていれば OK です。また、適当にフォームに入力し、送信してみてください。
このような画面が表示されたら正常です。画像だと見にくいので文字にすると以下のようになります。
array:2 [▼
0 => "This is thread title!"
1 => "This is first comment!"
]
送信ボタンを押すと ThreadController
の store
メソッドが呼び出されます。現在は dd
関数でフォームの内容を出力しているので、このような挙動になります。
それではスレッド作成機能を実装していきましょう。
Thread モデルと Threads テーブルの作成
スレッドのモデルとテーブルを作成していきます。
php artisan make:model Thread -m
-m
をつけてマイグレーションファイルも生成しています。
生成された Thread.php
とマイグレーションファイルを以下のように修正してください。
// App/Models/Thread.php
class Thread extends Model
{
use HasFactory;
+ protected $fillable = [
+ 'title'
+ ];
}
// database/migrations/2021_04_04_082524_create_threads_table.php
public function up()
{
Schema::create('threads', function (Blueprint $table) {
$table->id();
+ $table->foreignId('user_id')->constrained()->onDelete('cascade');
+ $table->text('title');
$table->timestamps();
});
}
マイグレートを実行しましょう。
php artisan migrate
また、User
モデルを以下のように修正します。
class User extends Authenticatable
{
use HasFactory, Notifiable;
/**
省略
/**
+ public function threads()
+ {
+ return $this->hasMany(Thread::class);
+ }
}
hasMany の関係を指示しておくことで user()→threads()→create()
のようにスレッドを作成できるようになります。
Comment モデルと Comments テーブルの作成
今回の掲示板では、スレッドの作成と同時に、1つ目のコメントを投稿するとします。また、各ユーザーはスレッドに対して自由にコメントできます。
php artisan make:model Comment -m
マイグレーションファイルを編集します。
// database/migrations/2021_04_04_085705_create_comments_table.php
public function up()
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
+ $table->foreignId('user_id')->constrained()->onDelete('cascade');
+ $table->foreignId('thread_id')->constrained()->onDelete('cascade');
+ $table->text('body');
$table->timestamps();
});
}
マイグレートを実行しましょう。
php artisan migrate
そして、Comment モデルと User モデルと Thread モデルを以下のように修正します。
class Comment extends Model
{
use HasFactory;
+ protected $fillable = [
+ 'body',
+ 'user_id'
+ ];
+ public function user()
+ {
+ return $this->belongsTo(User::class);
+ }
}
class User extends Authenticatable
{
use HasFactory, Notifiable;
/**
*
* 省略
*
*/
+ public function comments()
+ {
+ return $this->hasMany(Comment::class);
+ }
}
class Thread extends Model
{
use HasFactory;
protected $fillable = [
'title'
];
+ public function comments()
+ {
+ return $this->hasMany(Comment::class);
+ }
}
belongsTo の関係を指示しておくことで、$comment->user->name
のように、コメントの投稿者の名前を呼び出すことができます。
スレッド作成機能の実装
それでは store
メソッドの中身を書きましょう。
ThreadController
の store
メソッドを以下のように変更してください。
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string|max:512',
]);
DB::transaction(function () use ($request) {
$thread = $request->user()->threads()->create([
'title' => $request->title,
]);
$thread->comments()->create([
'body' => $request->body,
'user_id' => $request->user()->id
]);
});
return back();
}
また、DB ファサードを利用しているため、ファイル内の上部に以下の記述を追加してください。
use Illuminate\Support\Facades\DB;
やっていることは3つです。
- バリデーション
- データの保存
- リダイレクト
本来ならリダイレクト先はスレッドの詳細ページにしたいのですが、まだページを作成していないので、スレッド作成ページに戻しています。
また、ポイントはトランザクションです。もしトランザクションを使用しないと、スレッドの作成には成功したがコメントの作成には失敗、なんてことが起こりかねません。そのため、必ずトランザクションを行うようにしましょう。
これでスレッド作成機能が完成しました。実際にフォームを送信して、DB にスレッドとコメントが作成されていることを確認しましょう。phpMyAdmin から確認してもいいですし、TablePlus 等のツールもおすすめします。
TablePlus | Modern, Native Tool for Database Management.
スレッド詳細ページの作成
それではリダイレクト先のページを作成していきましょう。
テンプレートを作成しますが、左向き矢印の svg ファイルを使用している箇所があります。以下のページからアイコンをダウンロードし、public ディレクトリに置いてください。
それではまずテンプレートを作成します。
// resources/views/threads/show.blade.php
<x-app-layout>
<x-slot name="header">
<div class="flex items-center gap-x-4">
<a href="{{ route('threads') }}">
<img src="/left-arrow.svg" width="30" height="30" alt="">
</a>
<div class="max-w-4xl">
<h1 class="font-semibold text-xl text-gray-800 leading-tight">{{ $thread->title }}</h1>
</div>
</div>
</x-slot>
<div class="pt-6 max-w-4xl mx-auto grid bg-white mt-4">
@if ($comments->count())
@foreach ($comments as $comment)
<x-comment-card :comment="$comment" />
@endforeach
@else
No comments
@endif
</div>
</x-app-layout>
注意点としては、$thread
$comments
という変数を使用していることと、comment-card
というコンポーネントを使用していることです。まずはコンポーネントを作っていきましょう。
php artisan make:component CommentCard
コマンドを実行すると CommentCard.php
と comment-card.blade.php
が作成されます。
それぞれ以下のように編集します。
// app/View/components/CommentCard.php
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class CommentCard extends Component
{
public $comment;
public function __construct($comment)
{
$this->comment = $comment;
}
/**
*
* 省略
*
*/
}
// resources/views/components/comment-card.blade.php
@props(['comment' => $comment])
<div class="border-b-2 p-4">
<span class="text-sm font-bold">{{ $comment->user->name }}</span>
<span class="text-sm text-gray-600">{{ $comment->created_at->toDateTimeString() }}</span>
<p>{{ $comment->body }}</p>
</div>
comment プロパティとコンストラクタでの処理を CommentCard.php
に追加することで、コンポーネントを呼び出す際に comment プロパティを渡せるようになります。
それではコントローラーに処理を追加しましょう。
// app/Http/Controllers/ThreadController.php
public function show(Thread $thread)
{
$comments = $thread->comments()->with(['user'])->paginate(20);
return view('threads.show', [
'thread' => $thread,
'comments' => $comments
]);
}
このように記述することで、show.blade.php
で $thread
と$comments
を使用できます。
次にルートを追加します。
// routes/web.php
+ Route::get('/threads/{thread}', [ThreadController::class, 'show'])->name('threads.show');
これで準備は整いました。最語に、スレッドの作成に成功したら、今作ったページにリダイレクトするように store
メソッドを修正しましょう。
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string|max:512',
]);
$thread = DB::transaction(function () use ($request) {
$thread = $request->user()->threads()->create([
'title' => $request->title,
]);
$thread->comments()->create([
'body' => $request->body,
'user_id' => $request->user()->id
]);
return $thread;
});
return redirect()->route("threads.show", $thread);
}
修正した点は2つです。
- トランザクションが新しいスレッドのデータを返すようになった
- そのデータを用いて、詳細ページにリダイレクトした
それでは動作を確認しましょう。
送信します。
リダイレクトされ、スレッドとコメントが表示されました!
コメント投稿機能の実装
コメントを投稿できるようにしましょう。コントローラーを作成し、保存する処理を記述します。
php artisan make:controller CommentController
作成されたコントローラーの CommentController
クラスに、以下のメソッドを追加します。
public function store(Thread $thread, Request $request)
{
$request->validate([
'body' => 'required|string|max:512'
]);
$thread->comments()->create([
'body' => $request->body,
'user_id' => $request->user()->id
]);
return back();
}
次に、web.php
に以下のルートを追加します。
Route::post('/threads/{thread}/comments', [CommentController::class, 'store'])->name('comments.store');
ファイル上部でコントローラーを読み込むのも忘れないようにしてください。
use App\Http\Controllers\CommentController;
それではフォームを作成しましょう。
show.blade.php
を以下のように変更します。
<x-app-layout>
<x-slot name="header">
<div class="flex items-center gap-x-4">
<a href="{{ route('threads') }}">
<img src="/left-arrow.svg" width="30" height="30" alt="">
</a>
<div class="max-w-4xl">
<h1 class="font-semibold text-xl text-gray-800 leading-tight">{{ $thread->title }}</h1>
</div>
</div>
</x-slot>
<div class="pt-6 max-w-4xl mx-auto grid bg-white mt-4">
@if ($comments->count())
@foreach ($comments as $comment)
<x-comment-card :comment="$comment" />
@endforeach
@else
No comments
@endif
+ <form action="{{ route('comments.store', $thread) }}" method="POST" class="m-4">
+ @csrf
+ <label for="body">{{ __('Comment') }}</label>
+ <textarea name="body" id="body" cols="30" rows="4" class="w-full rounded-lg border-2 bg-gray-100 @error('comment') border-red-500 @enderror"></textarea>
+ <div class="mt-4">
+ <button type="submit" class="bg-blue-500 rounded font-medium px-4 py-2 text-white">{{ __('Submit') }}</button>
+ </div>
+ </form>
</div>
</x-app-layout>
実際にコメントを投稿してみましょう。
投稿できました!
スレッド一覧を表示する
再びスレッド一覧ページを開いてみましょう。スレッドを表示する処理を書いていないので、先程作成したスレッドが表示されていません。
ThreadController
の index
メソッドを修正しましょう。
public function index()
{
$threads = Thread::latest()->paginate(20);
return view('threads.index', [
'threads' => $threads
]);
}
これで index.blade.php
で $threads
を使用できます。
// resources/views/threads/index.blade.php
<x-app-layout>
<x-slot name="header">
<div class="flex items-center justify-between">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Threads') }}
</h2>
<div>
<a href="{{ route('threads.create') }}">{{ __('New Thread') }}</a>
</div>
</div>
</x-slot>
<div class="py-12 max-w-4xl mx-auto sm:px-6 lg:px-8 grid gap-y-2">
@if ($threads->count())
@foreach ($threads as $thread)
<x-thread-card :thread="$thread" />
@endforeach
@else
There is no thread.
@endif
</div>
</x-app-layout>
変更点は2つです。
- スレッド作成ページへのリンクを表示した
- スレッド一覧を表示した
ただ、 thread-card
コンポーネントを作成していないため、まだ表示できません。作成しましょう。要領は comment-card
コンポーネントを作成したときと同じです。
php artisan make:component ThreadCard
生成された ThreadCard.php
ファイルの ThreadCard
クラスにプロパティを追加し、
public $thread;
コンストラクタを修正します。
public function __construct(Thread $thread)
{
$this->thread = $thread;
}
thread-card.blade.php
は以下のようにします。
@props(['thread' => $thread])
<a href="{{ route('threads.show', $thread) }}" class="p-4 block grid bg-white sm:rounded-lg border-1 shadow-sm">
<span>
{{ $thread->title }}
</span>
<span class="text-gray-600 text-sm">
{{ $thread->created_at->diffForHumans() }}
</span>
</a>
スレッド一覧ページを見てみましょう。
スレッド作成ページへのリンクも、スレッド詳細ページへのリンクも、正常に動作しています。
ミドルウェアを使用する
現在の仕様には問題があります。ログインしていないユーザーもスレッドの作成やコメントの投稿を行うことができてしまいます。そこで、ミドルウェアを使用してユーザーの行動を制限します。
ThreadController クラスにコンストラクタを追加しましょう。
// app/Http/Controllers/ThreadController.php
// ThreadController の中に以下を追加
public function __construct()
{
$this->middleware('auth')->only(['create', 'store']);
}
これだけで要件を満たすことができます。試しにログアウトしてみましょう。トップページにリダイレクトされるので、アドレスバーから /threads
にアクセスしてみましょう。
一覧ページはちゃんと表示されます。それではスレッド作成ページにアクセスしてみましょう。アドレスバーから /threads/create
にアクセスするか、"New Thread" のリンクをクリックしてください。
ログインページにリダイレクトされました!
コメントの投稿にも同様の制限をかけましょう。
// app/Http/Controllers/CommentController.php
// CommentController クラスの中に以下を追加
public function __construct()
{
$this->middleware('auth')->only(['store']);
}
コメント削除機能の実装
練習として、削除機能も実装してみましょう。
- ユーザーは自分のコメントを削除できる
- 他人のコメントは削除できない
という機能にします。
CommentController に destroy
メソッドを追加します。
// app/Http/Controllers/CommentController.php
// CommentController クラスの中に以下を追加
public function destroy(Comment $comment)
{
$comment->delete();
return back();
}
ルートを追加します。
// routes/web.php
Route::delete('/comments/{comment}', [CommentController::class, 'destroy'])->name('comments.destroy');
comment-card コンポーネントにフォームを追加します。
@props(['comment' => $comment])
<div class="border-b-2 p-4">
<span class="text-sm font-bold">{{ $comment->user->name }}</span>
<span class="text-sm text-gray-600">{{ $comment->created_at->toDateTimeString() }}</span>
<p>{{ $comment->body }}</p>
+ <form action="{{ route('comments.destroy', $comment) }}" method="post" class="mt-2">
+ @csrf
+ @method('DELETE')
+ <button type="submit" class="text-blue-500">{{ __('Delete') }}</button>
+ </form>
</div>
ログインして、スレッド詳細ページにアクセスしましょう。
Delete ボタンをクリックしてみましょう。
コメントが削除されました!
ポリシーの作成
コメントの削除に制限をかけましょう。先ほども、スレッドとコメントの作成に制限をかけましたね。しかし、今回の要件はそれと異なります。ログインしているかどうかだけでなく、コメントを投稿したユーザーと Delete リクエストを送ったユーザーが一致しているかどうかも確認しなければいけません。
そこで、ポリシーという機能を利用します。
php artisan make:policy CommentPolicy
CommentPolicy.php
というファイルが生成されます。CommentPolicy クラスに、以下のメソッドを追加してください。
// app/Policies/CommentPolicy.php
// CommentPolicy クラスの中に以下を追加
public function delete(User $user, Comment $comment)
{
return $user->id === $comment->user_id;
}
これで準備ができました。コントローラーの destroty
メソッドを以下のように修正しましょう。
public function destroy(Comment $comment)
{
+ $this->authorize('delete', $comment);
$comment->delete();
return back();
}
一行追加しただけですが、これで削除制限が実装されました。
確認するために、ユーザーを追加します。一度ログアウトして、新しいユーザーを登録しましょう。そのままログインした状態で、スレッド詳細ページにアクセスしましょう。
Hanako という名前のユーザーを新たに登録しました。試しにいくつかコメントしてみましょう。
コメントできていますね。それでは、一番上にある Taro さんの投稿を削除してみましょう。先ほど作ったポリシーが適用されていれば、削除に失敗するはずです。
"THIS ACTION IS UNAUTHORIZED." と表示されましたね。ブラウザバックしてリロードしてみても、コメントが削除されていないことが確認できます!
それでは、削除が許可されている場合のみボタンが表示されるように修正しましょう。
@can('delete', $comment)
<form action="{{ route('comments.destroy', $comment) }}" method="post" class="mt-2">
@csrf
@method('DELETE')
<button type="submit" class="text-blue-500">{{ __('Delete') }}</button>
</form>
@endcan
フォームを @can
ディレクティブで囲みました。これで、許可されているユーザーにのみフォームが表示されます。
現在は Hanako でログインしているので、Taro のコメントには Delete ボタンが表示されていません!また、自分のコメントにはボタンが表示されています!
多言語対応
現在は、すべての単語を英語で表示しています。試しに、"Dashboard" という単語を日本語対応させてみましょう。
resources/lang/ja.json
ファイルを作成し、以下のように記述します。
// resources/lang/ja.json
{
"Dashboard": "ダッシュボード"
}
ダッシュボードページをリロードしましょう。
日本語表示になりましたね。これまで blade テンプレートファイルで、
{{ __('Dashboard') }}
のような書き方をしてきたかと思います。このように書いておけば、言語ごとに json ファイルを作成することで、多言語対応することができます。最初の方に config/app.php
ファイルで locale を "ja" に設定したのを覚えているでしょうか。あそこで設定したため、日本語ファイルがあれば日本語を優先して表示してくれます。
まとめ
これで今回の記事は終わりにします。私は React や Next.js が好きなのであまり Laravel には慣れていませんが、学んでみると楽しいものですね。ORM に興味が湧いたので、TS で書けるという Prisma も近々触ってみようと思います。ここまで読んでくださった方、ありがとうございました。
Discussion