In my current system a user can write a review for a product.
Below is the method where the validation and creation of the review takes place in my Controller:
public function PostAndValidate($id)
{
$input = [
'comment' => Input::get('comment'),
'rating' => Input::get('rating')
];
$review = new Review;
$validator = Validator::make( $input, $review->getCreateRules());
return to product page with error message
if ($validator->passes()) {
$review->storeReviewForBook($id, $input['comment'], $input['rating']);
return Redirect::to('book/'.$id.'#reviews-anchor')->with('review_posted',true);
}
return Redirect::to('book/'.$id.'#reviews-anchor')->withErrors($validator)->withInput();
}
How can I prevent the user to post a new review for a book or product where he or she already reviewed?
You can do the following in your storeReviewForBook
method:
$book = Book::find($id);
if(in_array(Auth::user()->id, $book->reviews->lists('user_id')->all())) {
return redirect()->back()->with('message', 'You already reviewed this book');
}
First off as a good habit, as much as possible put all your logic in the controllers. you dont need a storeReviewForBook in your Review model file.
I would write your postAndValidate function like so,
public function PostAndValidate($id)
{
$input = [
'comment' => Input::get('comment'),
'rating' => Input::get('rating')
];
$review = new Review;
$validator = Validator::make( $input, $review->getCreateRules());
if ($validator->passes()) {
//queries for a review with book id and user id matching the current transaction
$existing = Review::where('book_id','=',$id)
->where('user_id','=',Auth::user()->id)
->first();
//last query returns null if nothing is returned
if($existing!=null)
{
return redirect()->back()->with('message', 'You already reviewed this book');
}
else
{
$review->comment = $input['comment'];
$review->rating = $input['rating'];
$review->book_id = $id;
$review->user_id = Auth::user()->id;
$review->save();
return Redirect::to('book/'.$id.'#reviews-anchor')->with('review_posted',true);
}
return Redirect::to('book/'.$id.'#reviews-anchor')->withErrors($validator)->withInput();
}
The model should be the layer to interact with the database while you place your logic in the controller. its also more readable and easier to debug.
EDIT As a form of data integrity you could put a unique index in the user_id and the book_id in the reviews table. place user_id and book_id as an array so the unique index will be of the 2 columns combined.
//in migration file
$table->unique(['user_id','book_id']);