Advertisement

Building a 5 Star Rating System with jQuery, AJAX and PHP

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

In this tutorial, you'll learn how to build a rating system with AJAX, PHP, and jQuery. Votes will be recorded and updated in real-time with the magic of AJAX, and we'll also leverage the power of PHP so that you don't even need a database!


Step 1. Building the HTML

We're going to create a simple page that lists two movies, and allows you to rate them. This means we need the stars to show the current rating, and to allow voting. We also want an area to show the total votes cast, and the current rating down to one decimal place.

Let's take a look at the HTML/CSS

        <div class='movie_choice'>
            Rate: Raiders of the Lost Ark
            <div id="r1" class="rate_widget">
                <div class="star_1 ratings_stars"></div>
                <div class="star_2 ratings_stars"></div>
                <div class="star_3 ratings_stars"></div>
                <div class="star_4 ratings_stars"></div>
                <div class="star_5 ratings_stars"></div>
                <div class="total_votes">vote data</div>
            </div>
        </div>
        
        <div class='movie_choice'>
            Rate: The Hunt for Red October
            <div id="r2" class="rate_widget">
                <div class="star_1 ratings_stars"></div>
                <div class="star_2 ratings_stars"></div>
                <div class="star_3 ratings_stars"></div>
                <div class="star_4 ratings_stars"></div>
                <div class="star_5 ratings_stars"></div>
                <div class="total_votes">vote data</div>
            </div>
        </div>

Notice how there are no graphics in this HTML? They'll be added with CSS. We're just using the HTML to create the framework that the widget works from. Now it's time to start adding CSS.

        .rate_widget {
            border:     1px solid #CCC;
            overflow:   visible;
            padding:    10px;
            position:   relative;
            width:      180px;
            height:     32px;
        }
        .ratings_stars {
            background: url('star_empty.png') no-repeat;
            float:      left;
            height:     28px;
            padding:    2px;
            width:      32px;
        }
        .ratings_vote {
            background: url('star_full.png') no-repeat;
        }
        .ratings_over {
            background: url('star_highlight.png') no-repeat;
        }

This first part of the CSS accomplishes a few things:

  • Gives the default 'empty' start to each star location
  • Sets up classes for filled in stars, and highlighted stars
  • Defines and styles the stars' container.

You can either use the graphics provided in the download, or make your own. There needs to be a graphic for each of the three states: empty, full, and highlighted.

Next we add a little more CSS to position the total votes box, and center the widgets so the page matches the graphic at the start of this section.

        .total_votes {
            background: #eaeaea;
            top: 58px;
            left: 0;
            padding: 5px;
            position:   absolute;  
        } 
        .movie_choice {
            font: 10px verdana, sans-serif;
            margin: 0 auto 40px auto;
            width: 180px;
        }

Step 2. Adding the UI Interactivity

At this point, we have a very plain looking bunch of empty stars, but they don't do a whole lot at this point. This is where jQuery comes to the rescue.

Our first step is to add mouseover and mouseout handlers for the stars. We need to highlight the star the mouse is over, and all the preceding stars.

        $('.ratings_stars').hover(
            // Handles the mouseover
            function() {
                $(this).prevAll().andSelf().addClass('ratings_over');
                $(this).nextAll().removeClass('ratings_vote'); 
            },
            // Handles the mouseout
            function() {
                $(this).prevAll().andSelf().removeClass('ratings_over');
                set_votes($(this).parent());
            }
        );

We're taking advantage of jQuery's powerful .prevAll() and .nextAll() methods to get the stars preceding and following the currently moused over star.

The code above then adds and removes the classes to make the stars under the mouse and before 'highlighted', and the stars after 'not highlighted'.

What about set_votes() ?

This is a function that checks which stars should be in the 'full' state, and ties in closely with the next step, where we grab remote data from the server.


Step 3. Retrieving Data from the Server

Our stars highlight when you move the mouse over them, and that's a great start. But what about the red stars showing the current vote? To reach this step, we need to both get the information from the server, and write some JavaScript to handle that data.

        $('.rate_widget').each(function(i) {
            var widget = this;
            var out_data = {
                widget_id : $(widget).attr('id'),
                fetch: 1
            };
            $.post(
                'ratings.php',
                out_data,
                function(INFO) {
                    $(widget).data( 'fsr', INFO );
                    set_votes(widget);
                },
                'json'
            );
        });

This code block - actually all the JavaScript - goes in a document.ready block. This particular code executes right away. It queries the server and gets some information on every vote widget on the page.

First we set up an object, out_data, to contain the information we're sending to the server. Our PHP script expects to see 'fetch' when just grabbing data, so we include it here. We also include the ID of the widget, which lets the server-side script know what data we're after. When the call back function fires, it contains a JavaScript object that looks like this:

        {
            "widget_id"     : "r1",
            "number_votes"  : 129,
            "total_points"  : 344,
            "dec_avg"       : 2.7,
            "whole_avg"     : 3
        }

The .data() method is a bit of jQuery magic that allows you to associate arbitrary data with a DOM
object.

If you look closely at the code, you'll see we're taking that object (stored in the variable INFO) and
doing something with it via the .data() method.

The .data() method is a bit of jQuery magic that allows you to associate arbitrary data with a DOM
object. In this case, we're storing the data in the widget div. It can be accessed later like this:

        $('#one_of_your_widgets).data('fsr').widget_id;

set_votes(), Finally.

After the data has been returned from the server, its handed off indirectly to set_votes().

        function set_votes(widget) {
        
            var avg = $(widget).data('fsr').whole_avg;
            var votes = $(widget).data('fsr').number_votes;
            var exact = $(widget).data('fsr').dec_avg;
            
            $(widget).find('.star_' + avg).prevAll().andSelf().addClass('ratings_vote');
            $(widget).find('.star_' + avg).nextAll().removeClass('ratings_vote'); 
            $(widget).find('.total_votes').text( votes + ' votes recorded (' + exact + ' rating)' );
        }

The first three lines are for readability, as those variable names are pretty long. So let's take a look at what's happening here.

Line 7: 'avg' is a whole number, representing the rounded vote average of this widget. Because it's
a number 1-5, we can use it to find the proper star in the widget, and turn it, and the
preceding ones to our 'filled' graphic. Notice the use of .andSelf() to include the star that
we've selected.

Line 8: This is quite similar to line seven, but we're removing the filled graphic from later stars. This
is necessary in case the average for this widget has gone down since the last vote.

Line 9: Here we're updating the grey box underneath the widget, which shows a more precise rating,
and lets a visitor know how many votes have been cast.


Step 4. Let the Voting Begin

The final step for the UI is to enable voting. We're going to add a click handler to each of the stars. This click handler will be responsible for sending the vote data to the server.

Here's the click handler:

        $('.ratings_stars').bind('click', function() {
            var star = this;
            var widget = $(this).parent();
            
            var clicked_data = {
                clicked_on : $(star).attr('class'),
                widget_id : widget.attr('id')
            };
            $.post(
                'ratings.php',
                clicked_data,
                function(INFO) {
                    widget.data( 'fsr', INFO );
                    set_votes(widget);
                },
                'json'
            ); 
        });

In this code block, we start out by creating some variables not only for clarity, but, in this case, so they can be used within the .post callback. Remember the click handler is assigned to the stars, so we also need that second variable, widget, to have the object containing the data.

First, we set up our outgoing data, which we place in the object clicked_data. We grab the class which includes a class name in the format of star_# telling us what vote is being given, and prepare to send that to the server, along with the widget's ID.

The widget ID is the corner stone that this voting system relies on. It allows us to look up our stored data, and to easily show that data to the visitor.

Finally, on line line, we send this information to the server. The server will add the vote to the current totals, and send information back to the browser containing the updated data. The values displayed by the widget are then updated with set_votes().


Step 5. PHP: Creating the Class

Now that the UI is finished, we need to create a server side script to store and retrieve voting data.

We're going to create a very simple class in PHP, called 'Ratings,' and use it to handle server requests for our rating system. There are only going to be two methods, plus the invocation. The use of our class will look like so:

        # New Object
        $rating = new ratings($_POST['widget_id']);
    
        # either return ratings, or process a vote
        isset($_POST['fetch']) ? $rating->get_ratings() : $rating->vote();

If you go back to section four, you'll see we load the data with the variable 'fetch' set - that's what we're looking for here on line five. If its not set, then we're processing a vote.

The first thing we're going to look at is the begining of the class, and, more specifically, the constructor.

        class ratings {
            
            private $data_file = './ratings.data.txt';
            private $widget_id;
            private $data = array();
               
        function __construct($wid) {
            
            $this->widget_id = $wid;
        
            $all = file_get_contents($this->data_file);
            
            if($all) {
                $this->data = unserialize($all);
            }
        }

serialize() and unserialize are a great way to easily store
PHP data structures on disk.

There's a lot going on here in very few lines, so I'm going to cover the important bits.

Line 3: This needs to be set to a text file you'd like to use to store your data. We're not using a database for this project, although you easily could. A simple file will suffice for our needs.

Line 7: The constructor. This is called when we create our object, and immediately stores the ID of the widget.

Line 11: We try to load the text file. If the file doesn't exist, fine, but on some systems you'll need to create it ahead of time and give it the proper permissions for PHP to be able to read and write to it.

Line 14: This line is important. It takes the data from the text file - if there is one - and unserializes() it. The file contains a complex PHP array that's been converted to a plain text representation, via serialize(), allowing us to store it and read it back in as an array later.


Step 6. The get_ratings() Method.

This method is called either on its own, or from the vote() method. It finds the data for a particular widget ID and returns it to the requesting page, in JSON format.

    public function get_ratings() {
        if($this->data[$this->widget_id]) {
            echo json_encode($this->data[$this->widget_id]);
        }
        else {
            $data['widget_id'] = $this->widget_id;
            $data['number_votes'] = 0;
            $data['total_points'] = 0;
            $data['dec_avg'] = 0;
            $data['whole_avg'] = 0;
            echo json_encode($data);
        } 
    }

This only looks complicated - it's actually pretty simple. The first thing we do is check if the array stored in $this->data has a key matching our widget ID. If it does, we just return that information, because that's the widget data the page was requesting.

We don't have to do anything to that data because its already in array form. $this->data is just an array of arrays. We encode the array we want with json_encode() and send it back to the browser.

If there's no data for the widget ID we've requested, we create a record with all zero values, and send it back to the browser.

Step 7. The vote() Method

Next, we need to create a method to handle incoming votes. When the method finishes, it has to call get_ratings() to send the updated information back to the web browser.

The Method Start

        public function vote() {
            # Get the value of the vote
            preg_match('/star_([1-5]{1})/', $_POST['clicked_on'], $match);
            $vote = $match[1];

The first thing we do is get the value of the vote. Remember that somewhere in 'clicked_on' is a class name in the format of star_#. "star_4", for example. To get that value, we're using a regular expression and capturing the value of the number to $match[1].

The method Middle

    
            $ID = $this->widget_id;
            # Update the record if it exists
            if($this->data[$ID]) {
                $this->data[$ID]['number_votes'] += 1;
                $this->data[$ID]['total_points'] += $vote;
            }
            # Create a new one if it does not
            else {
                $this->data[$ID]['number_votes'] = 1;
                $this->data[$ID]['total_points'] = $vote;
            }

Here we store $this->widget_id into $ID for clarity - the following code gets a bit rough on the eyes without it.

We check if information for this ID exists, and, if so, we add a vote to the total vote count, and add the points from the vote received. This is a running total of all votes; so if one person gives five stars, and another, three, that's eight points total.

If the record doesn't exist, we create one, with one vote, and just the points from the incoming vote.

Finishing Up

  
            $this->data[$ID]['dec_avg'] = round( $this->data[$ID]['total_points'] / $this->data[$ID]['number_votes'], 1 );
            $this->data[$ID]['whole_avg'] = round( $this->data[$ID]['dec_avg'] );
                  
            file_put_contents($this->data_file, serialize($this->data));
            $this->get_ratings();
        }

Once we've updated the vote and point totals, we have to calculate both the average expressed as a whole number, and to one decimal point. To avoid having to do the math twice, we first calculate the average to one decimal on line one, and then round that off to a whole number, on line two.

On line four, we're storing the changed information back on disk after processing it with serialize(). Once the data is safely stored away, we call $this->get_ratings() to send the new, updated information to the browser.


Conclusion

For the sake of simplicity, this isn't a 100% complete solution. To extend this project, we should store a cookie to make sure people only vote once, or even record the IP address. It's also possible that two first-votes couple happen simultaneously, and only one may be recorded. It is, however, a great start, and is more then suitable for keeping track of votes on a few handfuls of items on your website. Thoughts? Thanks for reading!