【Laravel】いいね機能を実装する|簡単な掲示板アプリの作成
Laravelを使い、掲示板アプリに「いいね機能」を追加する手順を解説します。UserモデルとPostモデルを関連付けるLikeモデルを作成し、データベースの設計から始めます。コントローラーでいいねの登録と解除を制御し、Bladeでいいね数やボタンの状態を動的に表示する方法まで、一連の流れが学べます。
開発環境
- OS: Windows10
- Visual Studio Code: 1.73.0
- PHP: 8.3.11
- Laravel: 11.29.0
- laravel/breeze: 2.2
サンプルコード
/app/Http/Controllers/LikeController.php
/app/Http/Controllers/LikeController.php1+<?php 2+ 3+namespace App\Http\Controllers; 4+ 5+use Illuminate\Http\Request; 6+use App\Models\Post; 7+use Illuminate\Support\Facades\Auth; 8+ 9+class LikeController extends Controller 10+{ 11+ public function toggleLike(Post $post) 12+ { 13+ 14+ // ログインしていなければ、ログインページにリダイレクト 15+ if (!auth()->check()) { 16+ return redirect()->route('login'); 17+ } 18+ 19+ // すでにいいねしているか確認 20+ $user = Auth::user(); 21+ $like = $post->likes()->where('user_id', $user->id); 22+ 23+ if ($like->exists()) { 24+ // いいねを解除 25+ $like->delete(); 26+ } else { 27+ // いいねを追加 28+ $post->likes()->create(['user_id' => $user->id]); 29+ } 30+ 31+ return redirect()->back(); 32+ } 33+} 34
/app/Models/Like.php
/app/Models/Like.php1+<?php 2+ 3+namespace App\Models; 4+ 5+use Illuminate\Database\Eloquent\Model; 6+use Illuminate\Database\Eloquent\Factories\HasFactory; 7+ 8+class Like extends Model 9+{ 10+ use HasFactory; 11+ 12+ protected $fillable = [ 13+ 'user_id', 14+ 'post_id' 15+ ]; 16+ 17+ // Userモデルとのリレーション 18+ public function user() 19+ { 20+ return $this->belongsTo(User::class); 21+ } 22+ 23+ // Postモデルとのリレーション 24+ public function post() 25+ { 26+ return $this->belongsTo(Post::class); 27+ } 28+} 29
/app/Models/Post.php
/app/Models/Post.php1 { 2 return $this->hasMany(Comment::class); 3 } 4+ 5+ public function likes() 6+ { 7+ return $this->hasMany(Like::class); 8+ } 9 } 10
/app/Models/User.php
/app/Models/User.php1 { 2 return $this->hasMany(Comment::class); 3 } 4+ 5+ public function likes() 6+ { 7+ return $this->hasMany(Like::class); 8+ } 9 } 10
/database/migrations/2024_11_02_032950_create_likes_table.php
/database/migrations/2024_11_02_032950_create_likes_table.php1+<?php 2+ 3+use Illuminate\Database\Migrations\Migration; 4+use Illuminate\Database\Schema\Blueprint; 5+use Illuminate\Support\Facades\Schema; 6+ 7+return new class extends Migration 8+{ 9+ /** 10+ * Run the migrations. 11+ */ 12+ public function up(): void 13+ { 14+ Schema::create('likes', function (Blueprint $table) { 15+ $table->id(); 16+ $table->foreignId('user_id')->constrained()->onDelete('cascade'); 17+ $table->foreignId('post_id')->constrained()->onDelete('cascade'); 18+ $table->timestamps(); 19+ 20+ $table->unique(['user_id', 'post_id']); // ユーザーと投稿の組み合わせで一意にする 21+ }); 22+ } 23+ 24+ /** 25+ * Reverse the migrations. 26+ */ 27+ public function down(): void 28+ { 29+ Schema::dropIfExists('likes'); 30+ } 31+}; 32
/resources/views/layout.blade.php
/resources/views/layout.blade.php1 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 2 <title>掲示板アプリ</title> 3 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> 4+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> 5 {{-- <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> 6 <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.0.0/dist/umd/popper.min.js"></script> 7 <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> --}} 8
/resources/views/posts/show.blade.php
/resources/views/posts/show.blade.php1 @method('DELETE') 2 <button type="submit" class="btn btn-danger">削除</button> 3 </form> 4- @endif 5+ @endif 6 </div> 7 </div> 8 9+ {{-- いいねボタン --}} 10+ <form action="{{ route('posts.like', $post->id) }}" method="POST" style="display:inline;"> 11+ @csrf 12+ <button type="submit" class="btn {{ $post->likes->contains('user_id', auth()->id()) ? 'btn-danger' : 'btn-outline-danger' }}"> 13+ {!! $post->likes->contains('user_id', auth()->id()) ? '<i class="bi bi-heart-fill"></i>' : '<i class="bi bi-heart"></i>' !!} 14+ </button> 15+ </form> 16+ 17+ {{-- いいね数の表示 --}} 18+ <p>{{ $post->likes->count() }} 件のいいね</p> 19+ 20 {{-- コメント一覧 --}} 21 <h3>コメント一覧</h3> 22 @if ($comments->isEmpty()) 23
/routes/web.php
/routes/web.php1 use App\Http\Controllers\ProfileController; 2 use App\Http\Controllers\PostController; 3 use App\Http\Controllers\CommentController; 4+use App\Http\Controllers\LikeController; 5 use Illuminate\Support\Facades\Route; 6 7 Route::get('/', function () { 8 // コメントを投稿するルーティング 9 Route::post('posts/{post}/comments', [CommentController::class, 'store'])->name('comments.store'); 10 11+// いいねを投稿するルーティング 12+Route::post('/posts/{post}/like', [LikeController::class, 'toggleLike'])->name('posts.like')->middleware('auth'); 13+ 14 require __DIR__.'/auth.php'; 15
コード解説
変更点: いいね情報を保存するlikesテーブルの作成
/database/migrations/2024_11_02_032950_create_likes_table.php1+ public function up(): void 2+ { 3+ Schema::create('likes', function (Blueprint $table) { 4+ $table->id(); 5+ $table->foreignId('user_id')->constrained()->onDelete('cascade'); 6+ $table->foreignId('post_id')->constrained()->onDelete('cascade'); 7+ $table->timestamps(); 8+ 9+ $table->unique(['user_id', 'post_id']); // ユーザーと投稿の組み合わせで一意にする 10+ }); 11+ }
データベースに「いいね」の情報を保存するためのlikesテーブルを作成する設計図(マイグレーションファイル)です。
Schema::createメソッドを使ってテーブルの構造を定義しています。
id(): いいね一つ一つを識別するための番号(主キー)を自動で作成します。foreignId('user_id')->constrained()->onDelete('cascade'): 「誰が」いいねしたかを記録するためのuser_idカラムです。usersテーブルと連携しており、もしユーザーが退会などで削除された場合、そのユーザーのいいね情報も自動的に削除されます(onDelete('cascade'))。foreignId('post_id')->constrained()->onDelete('cascade'): 「どの投稿に」いいねしたかを記録するためのpost_idカラムです。postsテーブルと連携しており、投稿が削除された場合、その投稿へのいいねも自動的に削除されます。timestamps(): いいねが作成された日時(created_at)と更新された日時(updated_at)を自動で記録するカラムを作成します。unique(['user_id', 'post_id']): 一人のユーザーが同じ投稿に一度しか「いいね」できないようにするための制約です。user_idとpost_idの組み合わせが重複しないように保証します。
変更点: likesテーブルを操作するLikeモデルの作成
/app/Models/Like.php1+<?php 2+ 3+namespace App\Models; 4+ 5+use Illuminate\Database\Eloquent\Model; 6+use Illuminate\Database\Eloquent\Factories\HasFactory; 7+ 8+class Like extends Model 9+{ 10+ use HasFactory; 11+ 12+ protected $fillable = [ 13+ 'user_id', 14+ 'post_id' 15+ ]; 16+ 17+ // Userモデルとのリレーション 18+ public function user() 19+ { 20+ return $this->belongsTo(User::class); 21+ } 22+ 23+ // Postモデルとのリレーション 24+ public function post() 25+ { 26+ return $this->belongsTo(Post::class); 27+ } 28+}
先ほど作成したlikesテーブルを、Laravelのプログラム上で簡単に扱えるようにするための「Likeモデル」を新しく作成しています。モデルは、データベースのテーブルと1対1で対応するファイルです。
protected $fillable:createメソッドなどを使って一度にデータを保存・更新する際に、不正なデータが登録されるのを防ぐ仕組みです。ここに指定したuser_idとpost_idカラムのみ、一括での代入が許可されます。user()メソッド: 「1つのいいねは、1人のユーザーに属している」という関係(リレーション)を定義しています。belongsToは「〜に属する」という意味です。これにより、$like->userのようにして、いいねをしたユーザーの情報を簡単に取得できるようになります。post()メソッド: 「1つのいいねは、1つの投稿に属している」という関係を定義しています。これにより、$like->postのようにして、いいねされた投稿の情報を取得できます。
変更点: PostモデルとLikeモデルの関連付け
/app/Models/Post.php1+ public function likes() 2+ { 3+ return $this->hasMany(Like::class); 4+ }
Postモデル(投稿を扱うモデル)にlikes()メソッドを追加し、「1つの投稿は、たくさんのいいねを持つことができる」という関係(リレーション)を定義しています。
hasManyは「たくさん持っている」という意味です。この設定により、コントローラーやビューで$post->likesのように書くだけで、その投稿に紐づく全てのいいね情報を簡単に取得できるようになります。
変更点: UserモデルとLikeモデルの関連付け
/app/Models/User.php1+ public function likes() 2+ { 3+ return $this->hasMany(Like::class); 4+ }
Userモデル(ユーザーを扱うモデル)にも同様にlikes()メソッドを追加し、「1人のユーザーは、たくさんのいいねをすることができる」という関係(リレーション)を定義しています。
これにより、$user->likesのように書くことで、特定のユーザーが行った全てのいいね情報を簡単に取得できるようになります。
変更点: いいね・いいね解除を行うためのルート定義
/routes/web.php1 use App\Http\Controllers\CommentController; 2+use App\Http\Controllers\LikeController; 3 use Illuminate\Support\Facades\Route; 4 5 Route::get('/', function () { 6 // コメントを投稿するルーティング 7 Route::post('posts/{post}/comments', [CommentController::class, 'store'])->name('comments.store'); 8 9+// いいねを投稿するルーティング 10+Route::post('/posts/{post}/like', [LikeController::class, 'toggleLike'])->name('posts.like')->middleware('auth'); 11+ 12 require __DIR__.'/auth.php';
いいねボタンが押されたときに、どの処理を実行するかを定義する「ルート」をroutes/web.phpファイルに追加しています。
Route::post('/posts/{post}/like', ...):/posts/1/likeのようなURLに、POSTという方式でアクセスがあった場合のルールを定義しています。{post}の部分には、いいね対象の投稿IDが動的に入ります。[LikeController::class, 'toggleLike']: 上記のURLにアクセスがあったら、LikeControllerというファイルの中にあるtoggleLikeというメソッドを呼び出すように指定しています。name('posts.like'): このルートにposts.likeという名前を付けています。これにより、ビューファイル(HTML側)でURLを直接書く代わりに、この名前を使って簡単にURLを生成できます。middleware('auth'): このルートにアクセスするためにはログインが必要である、という制約を加えています。ログインしていないユーザーがいいねしようとすると、自動的にログインページに移動させられます。
変更点: いいね・いいね解除の処理を実装するLikeControllerの作成
/app/Http/Controllers/LikeController.php1+<?php 2+ 3+namespace App\Http\Controllers; 4+ 5+use Illuminate\Http\Request; 6+use App\Models\Post; 7+use Illuminate\Support\Facades\Auth; 8+ 9+class LikeController extends Controller 10+{ 11+ public function toggleLike(Post $post) 12+ { 13+ 14+ // ログインしていなければ、ログインページにリダイレクト 15+ if (!auth()->check()) { 16+ return redirect()->route('login'); 17+ } 18+ 19+ // すでにいいねしているか確認 20+ $user = Auth::user(); 21+ $like = $post->likes()->where('user_id', $user->id); 22+ 23+ if ($like->exists()) { 24+ // いいねを解除 25+ $like->delete(); 26+ } else { 27+ // いいねを追加 28+ $post->likes()->create(['user_id' => $user->id]); 29+ } 30+ 31+ return redirect()->back(); 32+ } 33+}
いいねの登録と解除を行う実際の処理を記述するLikeControllerを新しく作成しています。
toggleLike(Post $post): ルート定義の{post}部分のIDをもとに、Laravelが自動的に対象の投稿データをデータベースから探し出し、$post変数にセットしてくれます。$user = Auth::user():Auth::user()を使って、現在ログインしているユーザーの情報を取得します。$like = $post->likes()->where('user_id', $user->id): 対象の投稿($post)に紐づくいいね(likes())の中から、ログイン中のユーザーID($user->id)と一致するものを探します。if ($like->exists()):exists()メソッドで、いいねが見つかったかどうか(=すでにいいね済みか)を判定します。$like->delete(): もし、いいねが見つかった場合(いいね済みの場合)、そのいいね情報をデータベースから削除します。これが「いいね解除」の処理です。$post->likes()->create(['user_id' => $user->id]): もし、いいねが見つからなかった場合(まだいいねしていない場合)、新しくいいね情報を作成してデータベースに保存します。これが「いいね登録」の処理です。return redirect()->back(): いいね登録・解除の処理が終わったら、直前にいたページ(いいねボタンを押した投稿詳細ページ)にユーザーを戻します。
変更点: いいねアイコン表示用のCSS読み込み
/resources/views/layout.blade.php1 <title>掲示板アプリ</title> 2 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> 3+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> 4 {{-- <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> 5 <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.0.0/dist/umd/popper.min.js"></script> 6 <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> --}}
アプリケーションの全ページで共通のレイアウトを定義するlayout.blade.phpファイルに、1行のコードを追加しています。
これは、いいねボタンで使うハートのアイコン(❤)を表示するために、「Bootstrap Icons」というWebアイコン集のスタイルシート(CSS)を読み込むための記述です。このlinkタグを追加することで、Webページ全体でアイコン用のクラス名が使えるようになります。
変更点: 投稿詳細ページにいいねボタンといいね数を表示
/resources/views/posts/show.blade.php1+ {{-- いいねボタン --}} 2+ <form action="{{ route('posts.like', $post->id) }}" method="POST" style="display:inline;"> 3+ @csrf 4+ <button type="submit" class="btn {{ $post->likes->contains('user_id', auth()->id()) ? 'btn-danger' : 'btn-outline-danger' }}"> 5+ {!! $post->likes->contains('user_id', auth()->id()) ? '<i class="bi bi-heart-fill"></i>' : '<i class="bi bi-heart"></i>' !!} 6+ </button> 7+ </form> 8+ 9+ {{-- いいね数の表示 --}} 10+ <p>{{ $post->likes->count() }} 件のいいね</p>
投稿の詳細ページ(show.blade.php)に、いいねボタンといいね数を表示するコードを追加しています。
- いいねボタンのフォーム:
<form action="{{ route('posts.like', $post->id) }}" method="POST">: ボタンを押したときに、web.phpで定義したposts.likeルートにデータを送信するためのフォームです。$post->idを渡すことで、どの投稿に対するアクションかを伝えます。@csrf: Laravelのフォームに必須のセキュリティ対策です。
- ボタンの表示切り替え:
{{ $post->likes->contains('user_id', auth()->id()) ? 'btn-danger' : 'btn-outline-danger' }}:$post->likesで投稿に紐づく全いいねを取得し、contains('user_id', auth()->id())でその中にログイン中のユーザーのIDが含まれているか(=すでにいいねしているか)を判定しています。?と:を使った三項演算子という書き方で、条件によってクラス名を切り替えています。いいね済みならbtn-danger(赤く塗りつぶされたボタン)、まだならbtn-outline-danger(赤い枠線のボタン)が適用されます。
- アイコンの表示切り替え:
{!! ... ? '<i class="bi bi-heart-fill"></i>' : '<i class="bi bi-heart"></i>' !!}:- ボタンと同様の条件で、表示するアイコンを切り替えています。いいね済みなら
bi-heart-fill(塗りつぶされたハート)、まだならbi-heart(枠線だけのハート)を表示します。 {!! !!}は、<i ...>のようなHTMLタグをそのまま出力するための記法です。
- ボタンと同様の条件で、表示するアイコンを切り替えています。いいね済みなら
- いいね数の表示:
{{ $post->likes->count() }}:$post->likesで取得したいいね情報のコレクションに対して.count()メソッドを使い、その総数を数えて表示しています。
おわりに
今回は、likesテーブルの作成から、モデルのリレーション設定、コントローラーによる登録・解除処理、Bladeでの動的な表示切り替えまで、いいね機能実装の一連の流れを体験しました。UserモデルとPostモデルをLikeモデルでつなぐリレーションの考え方は、様々な機能開発の基礎となります。ビューでcontainsメソッドを使っていいね済みか判定し、ボタンの見た目を変える方法は、より良いUIを作るための実践的なテクニックです。ここで学んだデータベース、モデル、コントローラー、ビューの連携を、ぜひ今後の開発に活かしてください。