Sie sind auf Seite 1von 9

(HTTP://MAXOFFSKY.

COM/)
Home (http://maxoffsky.com) Code Blog (http://maxoffsky.com/category/code-blog/) Laravel Shop tutorial #1 Building a review system
Laravel Shop tutorial #1 Building a review system
POSTED 7 MONTHS AGO BY MAKS SURGUY (HTTP://MAXOFFSKY.COM/AUTHOR/MAXOFFSKY/)
0
The first tutorial in Building a shop (http://maxoffsky.com/code-blog/building-a-shop-
with-laravel-tutorial-series-announcement/) series is going to be a practical example
of implementing a rating/review system into an application. Review systems or rating
systems are very common in all kinds of e-Commerce projects, social networks,
anything that requires users input to be some sort of concrete value on some scale.
DEMO: http://laravel-shop.gopagoda.com/ (http://laravel-
shop.gopagoda.com/) Source on Github : https://github.com/msurguy/laravel-shop-
reviews (https://github.com/msurguy/laravel-shop-reviews)
A very common example of a review system is when the user can submit a rating of
1-5 stars and a comment that goes with the rating like on Etsy.com
(https://www.etsy.com/listing/168808125/instant-download-thanksgiving-cupcake?
ref=shop_home_active):
(https://www.etsy.com/listing/168808125/instant-download-thanksgiving-cupcake?
ref=shop_home_active)
Imagine this, you have an online shop that has products, products have
reviews that consist of a rating and a comment.
This is the idea that we will replicate in this tutorial.
HOME (HTTP://MAXOFFSKY.COM/) CODE BLOG (HTTP://MAXOFFSKY.COM/CATEGORY/CODE-BLOG/)
BLOG (HTTP://MAXOFFSKY.COM/CATEGORY/MAXOFFSKY-BLOG/) MY GITHUB (HTTP://MSURGUY.GITHUB.IO/)
PANORAMAS (HTTP://PANOPANDA.CO/MSURGUY) ABOUT MAKS (HTTP://MAXOFFSKY.COM/ABOUT/)
Heres how this tutorial will follow the steps to reach that goal:
1. See the Demo (http://laravel-shop.gopagoda.com/) of the project in action to get
an idea of what you will make (the link is above at the beginning of tutorial)
2. Think about efficiency
3. Define the routes for showing the products, showing individual product and
submitting the reviews
4. Define the database structure and the models for the products and for the reviews
5. Create the view templates to display the shop home page and the individual
product page.
6. Create functions that will calculate and store ratings/reviews
7. Create logic for the defined routes
8. Refactor the code if necessary
I have looked at lots of jQuery plugins that make it a bit easier to create a nice user
experience for rating a product by the number of stars/hearts, and I have found this
little plugin that does the job efficiently: https://github.com/dobtco/starrr
(https://github.com/dobtco/starrr) I have modified it to work with Bootstrap 3
(http://bootsnipp.com/snippets/featured/expanding-review-and-rating-box) and the
modified version is included in the source code (https://github.com/msurguy/laravel-
shop-reviews) of this application.
What are the difficulties of implementing such a review system?
Well, first of all efficiency is an important factor that we will have to include in making
architectural decisions. As an example, lets think about how the product will store
users reviews.
One way to store reviews is to store everything separately the rating, the comment
and the product each in a separate database table. We could relate the rating to the
comment (one to one relationship) and then store the comment under a product as
one to many relationship (a product has many reviews).
A disadvantage of this way of storing the data for reviews is that there is quite a bit
of overhead (a bit too much nesting which mean lots of inefficient SQL joins). Why
dont we combine the rating and the comment into one entity and call it Review?
Perhaps this will be a better structure for storing the comment and rating data
together.
When the user submits a review, we will store both a rating (consisting of number of
stars the user gives this product) and a comment (perhaps the user has something
to say about the product) as a new row in table designated to store our reviews.
We also need to somehow calculate an average rating for each product based on
submitted reviews and show it on products page and on the page that lists all
products.
Online shops usually have pages that display many products at once. Take for
example the homepage that shows a list of latest products. It is vital for shops
performance that displaying products and a review count/rating will be a quick task
for our application, with as few SQL queries as possible.
Taking this into an account, if we try to calculate the average rating for the product on
the fly we will run into performance issues. Therefore as a step of precaution, lets
save the calculated ratings in the products table (for example as rating_cache) as
well as saving all the reviews in the designated table. This way when we retrieve a
product from the database, we will have all necessary information to display right
away instead of executing too many unnecessary SQL queries.
Well, enough talking! Lets see the application in action! I have made the code
available on Github here (https://github.com/msurguy/laravel-shop-reviews) and the
live application is at http://laravel-shop.gopagoda.com/ (http://laravel-
shop.gopagoda.com/) play with it, leave some reviews for products and see how the
application feels.
As we have thought about the efficiency and have checked out the demo of the
application in action, lets define the routes for the application.
Defining the routes for the shop homepage, displaying the product and
submitting a review.
Our application will have 3 simple routes, a route that shows all products, a route that
shows individual product and a a route that is executed upon submittal of a review.
Lets open up the routes.php file and create a placeholder for these routes:
This seems like a good start. These route placeholders will be filled out as we are
progressing through the tutorial. Now lets define what the database for this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

// Route for Homepage - displays all products from the shop
Route::get('/', function()
{
return 'Shop homepage';
});

// Route that shows an individual product by its ID
Route::get('products/{id}', function($id)
{
return 'Product: '.$id;
});

// Route that handles submission of review - rating/comment
Route::post('products/{id}', array('before'=>'csrf', function
{
return 'Review submitted for product '.$id;
}));
?
progressing through the tutorial. Now lets define what the database for this
application will look like and what its data models will look like.
Defining the database structure and the data models:
Lets think a bit about where the data will live for the application. I have created a
simple diagram showing the different components of the product and reviews that
users will leave on the product. Please look at the diagram below.
(http://maxoffsky.com/word/wp-content/uploads/2013/11/Product-and-review-
structure-Decomposed.jpeg)
Lets define a database structure that will store the data according to the diagram
above.
The database will have the following tables:
Users (to store user information like name, email, password to log in)
Products (Will store product related data such as name, descriptions, icon for
the product, and finally rating cache and rating count)
Reviews (Will store reviews that users will leave for the product)
Here is the database structure visualized (click to see it bigger):
(http://www.laravelsd.com/share/CW5d1k)
And here is the SQL for the database, including some sample data for the products:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
CREATE TABLE `products` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`published` tinyint(1) NOT NULL DEFAULT '0',
`rating_cache` float(2,1) unsigned NOT NULL DEFAULT '3.0'
`rating_count` int(11) unsigned NOT NULL DEFAULT '0',
`name` varchar(255) NOT NULL,
`pricing` float(9,2) unsigned NOT NULL DEFAULT '0.00',
`short_description` varchar(255) NOT NULL,
`long_description` text NOT NULL,
`icon` varchar(255) NOT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

INSERT INTO `products` (`id`, `published`, `rating_cache`, `rating_count`, `
(1, 1, 3.0, 0, 'First product', 20.99, 'This is a short description asdf as This is a short description asdf as'
(2, 1, 3.0, 0, 'Second product', 55.00, 'This is a short description'
(3, 1, 3.0, 0, 'Third product', 65.00, 'This is a short description'
(4, 1, 3.0, 0, 'Fourth product', 85.00, 'This is a short description'
(5, 1, 3.0, 0, 'Fifth product', 95.00, 'This is a short description'

CREATE TABLE `reviews` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`rating` int(11) NOT NULL,
`comment` text NOT NULL,
`approved` tinyint(1) unsigned NOT NULL DEFAULT '1',
`spam` tinyint(1) unsigned NOT NULL DEFAULT '0',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
?
Whew! Now we can relate all this data together using Laravels models. Shall we?!
First, lets create the model for Products that will relate reviews by one to many
relationship, name it Product.php and place it into app/models folder:
Then lets relate the reviews to the products, to users and create some scopes
(http://laravel.com/docs/eloquent#query-scopes) for convenience, each review
belongs to one user, and review can belong to only one product, the scopes will
serve as shortcuts when we need to filter out reviews that are marked as spam(lets
say by the administrator), place the following into app/models/Review.php:
This is all we need for our relationships! We will add some additional functions into
the models a bit later to do the necessary rating calculations/storing of ratings, but
for now this will be enough for us to continue. Up next, creating view templates for
displaying the products and the reviews.
Creating view templates to show products and reviews
I assume you know a thing or two about Laravel at this point so I wont get too much
into building templates and layouts, but couple things I wanted to mention is that for
the review UI I have created a snippet (http://bootsnipp.com/msurguy/snippets/PjPa)
on my Bootsnipp titled Expanding review and rating box which you can see below
(using Bootstrap 3 for the user interface):
And for outputting user submitted reviews as a part of the product template I use the
following foreach loop:
The $review->timeago is a presenter function that I will add to the
37
38
39
40
41
42
43
44
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_type` int(10) unsigned NOT NULL DEFAULT '0',
`email` varchar(128) NOT NULL,
`password` varchar(128) NOT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
1
2
3
4
5
6
7
8
9
<?php

class Product extends Eloquent
{
public function reviews()
{
return $this->hasMany('Review');
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

class Review extends Eloquent
{

public function user()
{
return $this->belongsTo('User');
}

public function product()
{
return $this->belongsTo('Product');
}

public function scopeApproved($query)
{
return $query->where('approved', true);
}

public function scopeSpam($query)
{
return $query->where('spam', true);
}

public function scopeNotSpam($query)
{
return $query->where('spam', false);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@foreach($reviews as $review)
<hr>
<div class="row">
<div class="col-md-12">
@for ($i=1; $i <= 5 ; $i++)
<span class="glyphicon glyphicon-star{{ ($i <= $review->rating) ? '' : '-empty'}}"
@endfor

{{ $review->user ? $review->user->name : 'Anonymous'}} <span

<p>{{{$review->comment}}}</p>
</div>
</div>
@endforeach
?
?
?
app/models/Review.php:
Also, you might have noticed the triple brace used to output the $review->comment.
This is Laravels way to not allow things like <script></script> in displaying the
comments. I chose to not filter the comment data on input but rather when the
comment is displayed by using Blades triple braces trick.
You can see the complete view templates at https://github.com/msurguy/laravel-
shop-reviews (https://github.com/msurguy/laravel-shop-reviews).
At this point there are only few things left to finish, store the users reviews somehow
and also connect all the routes to the functionality that we have built.
Creating helper functions to store reviews and calculate ratings
First, lets create a helper function (lets call it storeReviewForProduct) that will do
the following :
take product ID, user submitted comment and rating
attach the comment and the rating as a child model to the product
call a function to recalculate all ratings for the product
This function will be a part of the app/models/Review.php model, so lets add it there:
For now we will not save logged in users id along with the comment simply because
we have not built a login system.
As you might have noticed, we are calling recalculateRating function on the product
instance, that function is not defined yet, so lets create it in the
app/models/Product.php model (I include comments that explain what happens when
that function is executed):
Alright. The major part of the application is done! The only thing left is to connect all
of our routes so that the applications becomes alive!
Connecting routes to views
In the beginning of the tutorial we have defined three route placeholders for the
application, one that shows all products, one that shows an individual product and
one thats handling the review submission. Now lets fill those routes with some logic
that the application needs.
Home page route only needs to display all products from the shop:
The route to display a single product (when you click on a product from the
homepage) needs to do two things, it needs to retrieve a product by id specified in
1
2
3
4
5
6
7
8
// Attribute presenters
...
public function getTimeagoAttribute()
{
$date = CarbonCarbon::createFromTimeStamp(strtotime($this
return $date;
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...

public function getTimeagoAttribute()
{
...
}

// this function takes in product ID, comment and the rating and attaches the review to the product by its ID, then the average rating for the product is recalculated
public function storeReviewForProduct($productID, $comment
{
$product = Product::find($productID);

// this will be added when we add user's login functionality
//$this->user_id = Auth::user()->id;

$this->comment = $comment;
$this->rating = $rating;
$product->reviews()->save($this);

// recalculate ratings for the specified product
$product->recalculateRating();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

class Product extends Eloquent
{
public function reviews()
{ ... }

// The way average rating is calculated (and stored) is by getting an average of all ratings,
// storing the calculated value in the rating_cache column (so that we don't have to do calculations later)
// and incrementing the rating_count column by 1

public function recalculateRating()
{
$reviews = $this->reviews()->notSpam()->approved();
$avgRating = $reviews->avg('rating');
$this->rating_cache = round($avgRating,1);
$this->rating_count = $reviews->count();
$this->save();
}
}
1
2
3
4
5
6
// Route for Homepage - displays all products from the shop
Route::get('/', function()
{
$products = Product::all();
return View::make('index', array('products'=>$products));
});
?
?
?
?
Bio Latest Posts
Share or like
this:
(http://maxoffsky.com/code-blog/laravel-shop-tutorial-1-building-a-review-system/?
share=twitter&nb=1)
(http://maxoffsky.com/code-blog/laravel-shop-tutorial-1-building-a-review-system/?
share=facebook&nb=1)
(http://maxoffsky.com/code-blog/laravel-shop-tutorial-1-building-a-review-system/?
share=email&nb=1)
(http://maxoffsky.com/code-blog/laravel-shop-tutorial-1-building-a-review-system/?
share=google-plus-1&nb=1)
(http://maxoffsky.com/code-blog/laravel-shop-tutorial-1-building-a-review-system/?
share=reddit&nb=1)
(http://maxoffsky.com/code-blog/laravel-shop-tutorial-1-building-a-review-system/?
share=pinterest&nb=1)
(http://maxoffsky.com/code-blog/laravel-shop-tutorial-1-building-a-review-system/?
share=pocket&nb=1)
the route parameter and it needs to retrieve a paginated collection of reviews that
are marked as approved and not spam:
And lastly, when the review is submitted from the product page, we will validate that
the review corresponds to validation rules(I put them in the Review model to make
the route a bit cleaner), and if the validation passes, we will store the review in DB
and redirect the user back to the product page with a message that the review has
been posted:
For a complete routes.php file please look in the application repository
(https://github.com/msurguy/laravel-shop-reviews/blob/master/app/routes.php). I
have omitted showing complete blade templates in this post as they are quite large,
you can see them in the repository as well.
Refactoring ideas :
1) Move the logic from the routes to controllers (for example create shop controller
and product controller and display index page from shop controller and all things
related to individual product from a product controller).
2) Clean up the models by separating view presenters scopes and relationships.
3) Prevent the same user from submitting more than one review on a product (when
you have a login system implemented first)
At this point the application should work as intended and you will be able to have
users submit reviews to your shop/gallery/whatever website!
This concludes the first part of Building a shop comprehensive tutorial! Do you like
the series? Do you want more of comprehensive tutorials like this? Let me know in
the comments and make sure to follow me on Twitter (http://twitter.com/msurguy)!
Maks Surguy (http://maxoffsky.com)
Full stack web developer, speaker and writer.
Maks is young and energetic breakdancer turned into web
developer who lives in Seattle area with his wife. He is well-versed
in three languages (English, Russian, Ukrainian) and a dozen of
programming languages.
POSTED IN CODE BLOG (HTTP://MAXOFFSKY.COM/CATEGORY/CODE-BLOG/) TAGGED: E-COMMERCE
(HTTP://MAXOFFSKY.COM/TAG/E-COMMERCE/), LARAVEL (HTTP://MAXOFFSKY.COM/TAG/LARAVEL/),
LARAVEL SHOP (HTTP://MAXOFFSKY.COM/TAG/LARAVEL-SHOP/), QUERY
(HTTP://MAXOFFSKY.COM/TAG/QUERY/), RATING (HTTP://MAXOFFSKY.COM/TAG/RATING/), RATING SYSTEM
(HTTP://MAXOFFSKY.COM/TAG/RATING-SYSTEM/), RATINGS (HTTP://MAXOFFSKY.COM/TAG/RATINGS/),
1
2
3
4
5
6
7
8
9
// Route that shows an individual product by its ID
Route::get('products/{id}', function($id)
{
$product = Product::find($id);
// Get all reviews that are not spam for the product and paginate them
$reviews = $product->reviews()->with('user')->approved()->notSpam()->orderBy(

return View::make('products.single', array('product'=>$product
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Route that handles submission of review - rating/comment
Route::post('products/{id}', array('before'=>'csrf', function
{
$input = array(
'comment' => Input::get('comment'),
'rating' => Input::get('rating')
);
// instantiate Rating model
$review = new Review;

// Validate that the user's input corresponds to the rules specified in the review model
$validator = Validator::make( $input, $review->getCreateRules());

// If input passes validation - store the review in DB, otherwise return to product page with error message
if ($validator->passes()) {
$review->storeReviewForProduct($id, $input['comment'], $input
return Redirect::to('products/'.$id.'#reviews-anchor')->with(
}

return Redirect::to('products/'.$id.'#reviews-anchor')->withErrors(
}));
(http://twitter.com/msurguy)
(https://plus.google.com/u/0/101547325374905024753?
rel=author)
(http://www.linkedin.com/in/makssurguy/)
Twitter 16
Facebook 25
Email
Google
Reddit
Pinterest
Pocket
More
?
?
Share your desktop or iOS device with anyone, anywhere. Try it free for 30 days!
(http://srv.buysellads.com/ads/click/x/GTND423ECKBD453LCYBLYKQWCT7IVKQMCWYDTZ3JCEYDC23EF67DLK3KC6BDTK7WC6SDEK3EHJNCLSIZ?
segment=placement:maxoffskycom)
FIND ON MAXOFFSKY
Search...
ADVERTISEMENT
(http://srv.buysellads.com/ads/click/x/GTND423ECKBD453LCYBLYKQWCT7IVKQMCWYDTZ3JCEYDC23EF67DLK3KC6BDTK7WC6SDEK3EHJNCLSIZ?
segment=placement:maxoffskycom)
ENJOYING MY POSTS OR PRODUCTS? GIVE
BACK HERE!
2

Gittip
MY BOOK:
I am writing a book about integrating
frontend components and libraries.
Readers of my blog can get it at 20%
off here:
(http://leanpub.com/frontend/c/niceblog)
MY SITES:
REVIEW(HTTP://MAXOFFSKY.COM/TAG/REVIEW/), SHOP (HTTP://MAXOFFSKY.COM/TAG/SHOP/), STAR
(HTTP://MAXOFFSKY.COM/TAG/STAR/), TUTORIAL (HTTP://MAXOFFSKY.COM/TAG/TUTORIAL/)
Integrating Facebook Login into
Laravel application
4 comments a month ago
Raghu Prince Awesome tutorial!!!!
Thanks Maks
Free wallpapers and a generator of
Delaunay triangulation patterns
1 comment a month ago
Michelle Alves
WOOOOOOOW!!!!!!!!!!!!!! this is f**g
amazing!!! tks, dude!!!
Sending E-Mail with Laravel 4 using
mail
1 comment a month ago
VitaliiSestrinskiy thanks
Uploading files in Laravel 4
4 comments 23 days ago
maxsurguy thanks! Will add that to the
post as an "FYI"
ALSOONMAKS SURGUY'S BLOG
0 Comments Maks Surguy's blog Login
Sort by Newest Share
Start the discussion
Be the first to comment.
WHAT'S THIS?
Subscribe Add Disqus to your site
Favorite
(http://bootsnipp.com)
(http://cheatsheetr.com)
(http://filegr.am)
(http://mymaplist.com)
(http://www.laravel-tricks.com)
SUBSCRIBE TO THIS BLOG VIA RSS
(http://maxoffsky.com/feed/) RSS
- Posts (http://maxoffsky.com/feed/)
UPDATES
Once a month updates from Maks.
No spam. No bullshit.
Email address:
Your email address
Sign up
FOLLOW ME ON TWITTER!
GitHub offers free micro plan+ for
students/teachers/researchers. Grab a
coupon at education.github.com! So
many people don't know!
Retweeted by Maks Surguy
Mu-An Chiou
@muanchiou
Show Summary
Dynamic Menu Builder for Bootstrap 3
and @laravelphp by @ilavary:
bit.ly/1pWj8hf
Retweeted by Maks Surguy
SitePoint PHP
@SitePointPHP
Show Summary
I've been working very hard on the
leanpub.com/frontend chapter about
AJAXFile Uploads. Available soon!
pic.twitter.com/FV3bE1l9gO
Maks Surguy
@msurguy
Show Photo
Discussing the past, present, and future
with the talented @msurguy (with a
shout out to @rajadain) :P
pic.twitter.com/SyPXLyM2c3
Kirupa Chinnathambi
@kirupa
5h
3h
5h
6h
Tweets Follow
Tweet to @msurguy
RECENTLY POSTED
Laracon impressions, memories,
takeaways
(http://maxoffsky.com/maxoffsky-
blog/laracon-impressions-memories-
blog/laracon-impressions-memories-
takeaways/)
Book review Building Secure PHP
Apps by Ben Edmunds
(http://maxoffsky.com/code-
blog/book-review-building-secure-
php/)
A list of CMSs built with Laravel
(http://maxoffsky.com/code-blog/list-
cmss-built-laravel/)
Free wallpapers and a generator of
Delaunay triangulation patterns
(http://maxoffsky.com/maxoffsky-
blog/free-wallpapers-generator-
delaunay-triangulation-patterns/)
Creating bar graphs with AJAX and
Morris library
(http://maxoffsky.com/code-
blog/creating-bar-graphs-with-ajax-
and-morris-laravel/)
WATCH MAKS BREAKDANCE!
(http://www.youtube.com/watch?
v=s_b-6265knA)

Das könnte Ihnen auch gefallen