
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、この間「入荷したらメールでお知らせ」機能をつくるという記事を公開しましたが、記事の冒頭でも書いたとおりネットサーフィンしていて「あこれつくってみたい」となったことが始まりでした。
そして、同じくAmazon
でもウィンドウショッピングしてたら、また「これもつくってみたい」と思う機能を発見しました。
それが・・・・・
レビュー機能(星とコメント)
です。
皆さんもよく知ってのとおりAmazon
でお買い物をすると、その商品が良かったかどうかを星(5段階)と文章で投稿することができます。
今回はそのレビュー機能をLaravel
でつくってみたいと思います。
ぜひ皆さんのお役に立てると嬉しいです
「レビュー機能って
考えた人すごいですよね」
開発環境: Laravel 7.x
目次 [非表示]
前提として
Laravel
にログイン機能がインストールされていて、テストユーザーが用意されている必要があります。
もしまだ準備されていない方は「Laravel6.x以降でログイン機能をインストールする方法」を参考にしてみてください。
データベースはこんなカンジです。
モデル&マイグレーションをつくる
まずデータベースまわりからやっていきます。
今回必要になるのは、次の2つです。
- Product: 商品
- ProductReview:商品のレビュー
つまり、Product
とProductReview
は「1:多」の関係になります。
では、Product
からです。
以下のコマンドを実行してください。
php artisan make:model Product -m
コマンドを実行したら、まずモデルに「1:多」のリレーションシップを追加します。
/app/Product.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
// リレーションシップ
public function reviews() { //
追加
return $this->hasMany(\App\ProductReview::class, 'product_id', 'id');
}
}
そして、マイグレーションは次のようにします。
/database/migrations/****_**_**_******_create_products_table.php
// 省略
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('商品名'); //
追加
$table->timestamps();
});
}
// 省略
次に、ProductReview
です。
同じく以下のコマンドを実行してください。
php artisan make:model ProductReview -m
こちらにも、users
テーブルとのリレーションシップをつくります。レビューはユーザーに所属しているのでbelongsTo
にします。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class ProductReview extends Model
{
// リレーションシップ
public function user() { //
追加
return $this->belongsTo(\App\User::class, 'user_id', 'id')
->select('id', 'name');
}
}
そして、マイグレーションもこのように変更してください。
/database/migrations/****_**_**_******_create_product_reviews_table.php
// 省略
public function up()
{
Schema::create('product_reviews', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('product_id')->default(0)->comment('商品ID');
$table->unsignedBigInteger('user_id')->default(0)->comment('ユーザーID');
$table->integer('stars')->default(0)->comment('星');
$table->text('comment')->comment('コメント');
$table->timestamps();
$table->foreign('product_id')->references('id')->on('products');
$table->foreign('user_id')->references('id')->on('users');
$table->unique(['product_id', 'user_id']);
});
}
// 省略
では、この状態で一度マイグレーションしておきます。
php artisan migrate
データベースはこうなります。
テストデータをつくる
次にProduct
にはテストデータがほしいのでSeeder
をつくっていきます。
以下のコマンドを実行してください。
php artisan make:seed ProductsTableSeeder
中身は次のようにします。
/database/seeds/ProductsTableSeeder.php
// 省略
public function run()
{
for($i = 1 ; $i <= 25 ; $i++) { //
追加
$product = new \App\Product();
$product->name = 'テスト商品名 - '. $i;
$product->save();
}
}
// 省略
続いて、ProductsTableSeeder
をLaravel
に登録します。
/database/seeds/DatabaseSeeder.php
// 省略
public function run()
{
//$this->call(UsersTableSeeder::class);
$this->call(ProductsTableSeeder::class); //
追加
}
// 省略
では、以下のコマンドでテストデータの追加をしましょう。
php artisan migrate:fresh --seed
データベースはこのようになります。
ルートをつくる
では次に、アクセスできるルートを登録します。
Route::get('product', 'ProductController@index');
Route::get('product/list', 'ProductController@list');
Route::post('product/review', 'ProductController@review');
1行目が直接ブラウザでアクセスするもので、2行目がAjax
を商品データの取得、そして3行目がレビューを投稿するルートになります。
コントローラーをつくる
では、ルートで設定したProductController
を作っていきます。
以下のコマンドを実行してください。
php artisan make:controller ProductController
するとファイルが作成されるので、中身を以下のように変更します。
/app/Http/Controllers/ProductController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function index() {
return view('product.index');
}
public function list() {
return \App\Product::with('reviews.user')->get();
}
public function review(Request $request) {
$result = false;
// バリデーション
$request->validate([
'product_id' => [
'required',
'exists:products,id',
function($attribute, $value, $fail) use($request) {
// ログインしてるかチェック
if(!auth()->check()) {
$fail('レビューするにはログインしてください。');
return;
}
// すでにレビュー投稿してるかチェック
$exists = \App\ProductReview::where('user_id', $request->user()->id)
->where('product_id', $request->product_id)
->exists();
if($exists) {
$fail('すでにレビューは投稿済みです。');
return;
}
}
],
'stars' => 'required|integer|min:1|max:5',
'comment' => 'required'
]);
$review = new \App\ProductReview();
$review->product_id = $request->product_id;
$review->user_id = $request->user()->id;
$review->stars = $request->stars;
$review->comment = $request->comment;
$result = $review->save();
return ['result' => $result];
}
}
この中で重要なのがreview()
メソッドです。
review()
では、まず送信内容をバリデーションでチェックすることになるのですが、product_id
に「独自バリデーション」が設定されていることに注目してください。
この独自バリデーションでは、以下の2つをチェックしています。
- ログインしているか
- レビュー投稿はまだ投稿していないか
つまり、ログインしていなければユーザーIDが取得できないので実行を拒否し、さらに同じ商品に重複してレビュー投稿させないようにしています。
※なお、独自バリデーションを別の場所でも使う場合は、今回のような書き方はせず、AppServiceProvider
にしっかり独自バリデーションを定義する方がいいでしょう。(使い回しができますので)
ビューをつくる
では最後にビュー(HTML部分)です。
<html>
<head>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div id="app" class="p-3">
<h1 class="mb-3">レビュー(星&コメント)機能・サンプル</h1>
<table class="table table-bordered mt-4">
<thead class="bg-info text-white">
<tr>
<th>ID</th>
<th>商品名</th>
<th>レビュー</th>
</tr>
</thead>
<tbody>
<!-- 商品データをリスト表示・・・ ① -->
<tr v-for="p in products">
<td v-text="p.id"></td>
<td v-text="p.name"></td>
<td>
<a href="#" type="button" v-if="!hasMyReview(p.reviews)" @click.prevent="openReviewForm(p.id)">
レビューを投稿
</a>
<!-- レビューをリスト表示 ・・・ ② -->
<div v-for="r in p.reviews">
<div class="p-3 bg-light mt-2">
<span class="font-weight-bold" v-text="r.user.name"></span>
<v-star :value="r.stars"></v-star>
<div class="text-break mt-1" style="white-space:pre;" v-text="r.comment"></div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<!-- レビュー投稿のモーダル ・・・ ③ -->
<div class="modal fade" id="review-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="review-modalLabel">💬 レビューの投稿</h5>
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<h4>スター</h4>
<div v-for="star in [5,4,3,2,1]">
<input v-model="reviewParams.stars" type="radio" :value="star">
<v-star :value="star"></v-star>
</div>
</div>
<div class="form-group">
<h4>コメント</h4>
<textarea class="form-control" v-model="reviewParams.comment"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link mr-2" data-dismiss="modal">閉じる</button>
<button type="button" class="btn btn-warning" @click="onSubmit">登録する</button>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
<script>
// 星を表示するコンポーネント ・・・ ④
Vue.component('v-star', {
props: ['value'],
template: '<span><span v-for="number in parseInt(value)">⭐</span></span>'
});
new Vue({
el: '#app',
data: {
userId: parseInt('{{ auth()->user()->id ?? -1 }}'), // ログイン・ユーザーID ・・・ ⑤
products: [],
reviewParams: {
product_id: '',
stars: '',
comment: ''
}
},
methods: {
getProducts() {
axios.get('/product/list')
.then(response => {
this.products = response.data;
});
},
openReviewForm(productId) {
this.reviewParams = {
product_id: productId,
stars: '',
comment: ''
};
$('#review-modal').modal('show');
},
hasMyReview(reviews) { // すでに投稿済みかどうかのチェック ・・・ ⑥
for(let review of reviews) {
if(this.userId === parseInt(review.user_id)) {
return true;
}
}
return false;
},
onSubmit(productId) {
axios.post('/product/review', this.reviewParams)
.then(response => {
if(response.data.result === true) {
this.getProducts();
$('#review-modal').modal('hide');
}
})
.catch(error => {
// エラー処理
alert('入力内容が正しくありません。');
console.log(error);
});
}
},
mounted() {
this.getProducts();
}
});
</script>
</body>
</html>
この中でやっていることは次のとおりです。
①商品データをリスト表示
Ajax
でProductController
のlist()
から取得した商品データをv-for
でループさせながらリスト表示しています。
②レビューをリスト表示
商品データとレビューは「1:多」の関係にあるので、ここでもv-for
でレビュのデータをループさせてリスト表示します。(つまり、ループの中にループがあることになります)
③レビュー投稿のモーダル
今回は「レビューを投稿」というリンクをクリックするとモーダル(ポップアップ)が表示され、そこで星と文章を入力する形にしますので、bootstrap
が用意してくれているモーダル機能を使います。
④星を表示するコンポーネント
複数の場所で1〜5の星を表示をするのでVue
コンポーネントを作っておくことにします。こうすることで重複する同じ記述をなくすことができます。
つまり、次のようにすると、
<v-star value="5"></v-star>
表示はこのようになります。
⑤ログイン・ユーザーID
ここは、次の⑥で使うことになるユーザーIDを取得するのですが、「PHP」の書き方に注目してください。
ここの流れとしては、
- auth()->user()->idから値が取得出来るかチェック
- 取得できるならその値を、
- もし取得できないなら-1を表示する
となります。
さらにページ表示後の「JavaScript」ではその値をparseInt()
で文字列→数値に変換してVue
データとして使うことになります。
例はこんなカンジです。
userId: parseInt('2') //
ユーザーID
⑥すでに投稿済みかどうかのチェック
ここでは、すでにそのログインユーザーがレビューを投稿済みかどうかをチェックし、もし投稿済みなら「レビューを投稿」というリンクは表示しないようになっています。
テストしてみる
では実際にテストしてみましょう
まず、ブラウザで「http://*****/product」にアクセスします。
では、太郎さんでログインして「テスト商品名 – 1」にレビューを投稿してみましょう。
今回はスターを5にし、コメントを入力して送信してみます。
すると、レビューが登録されて表示されました。
では、ログアウトして次は「次郎さん」でログインして表示してみます。
太郎さんのときは非表示になっていた「レビューを投稿」リンクが表示されました。
では、このまま次の入力で投稿してみましょう。
すると、太郎さんと次郎さんのレビュー内容が表示されました。
成功です
教材ソースコードをダウンロードする
今回実際に開発したソースコード一式を以下からダウンロードすることができます。
Laravelでレビュー機能をつくる※ただし、マイグレーションなどはご自身で実行していただく必要があります。
おわりに
ということで、今回はAmazon
にあるようなレビュー機能をLaravel
でつくってみました。
もしネットショップなどを構築したい場合は重要な機能になると思いますし、商品でなくてもフィードバックを得るには結構使えるんじゃないでしょうか。
ぜひ皆さんもやってみてくださいね。
ではでは〜
「僕は、Amazonとかのポイントは
貯めずに使い切る派です」