Ajax and jQuery tutorials for Symfony2 - by TipoCode


Symfony2 Ajax Lab - jQuery Plugins for Symfony2

Home Ajax - jQuery Star Rating System & symfony2

Ajax - jQuery Star Rating System & symfony2

How to add a 0.01 precise Symfony2 Multi-Star Rating System using jQuery and Ajax

I recommend you to read this tutorial to see how I created this star rating system on a procedural environment. Once you have read it and know how it works you may go further and see how I adapted it for a Symfony2 framework.

For the full tutorial explanation, click here.

Demo

Four items / medias to rate with a 5-stars rating system. You may set up the star number you want in the system.

Item 1

Rating: 3.82/5 (411 votes)

Item 2

Rating: 3.81/5 (423 votes)

Item 3

Rating: 3.65/5 (342 votes)

Item 4

Rating: 3.5/5 (262 votes)

Code

Bundle name: Sim100/TutoBundle

Layout

First, somewhere in your layout, at the bottom of your page, inside the {% block javascripts %} block, please add:

Notice the jquery.cookie.js, we will need, after someone vote, to place a cookie, so this person can't vote again for a certain period. I use the jQuery Cookie Plugin. More information below at the JS section.

<script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
<script src="{{ asset('bundles/tuto/js/jquery.cookie.js') }}"></script>
<script src="{{ asset('bundles/tuto/js/precise-star-rating.js') }}"></script>
<script>
    var loader = "{{ asset('bundles/tuto/images/loader-small.gif') }}"; //link to the animated loader-small.gif
    var ROOT_URL = "{{ url('tuto_homepage')}}"; //your root URL, used in autocomplete-countries.js file
</script>

JS

bundles/tuto/js/precise-star-rating.js

I am not gona explain this JS code here. This is something I have already explained in my previous tutorial here. Lines are also commented. Just check the tree functions:

  • rateMedia: when someone click on a star to rate (there is the AJAX call for data insertion in the database)
  • overStar: when you pass your mouse over stars (they become blue)
  • outStar: when you move your mouse out of a star (the blue over is removed)

Also, as I said above, I place a cookie after one votes, with the function $.cookie();. Please add the jQuery Cookie Plugin which allows you to create, read, and erase cookies with jQuery.

function rateMedia(mediaId, rate, numStar, starWidth) {
    $('#group' + mediaId + ' .star_bar #' + rate).removeAttr('onclick'); // Remove the onclick attribute: prevent multi-click
    $('.box' + mediaId).html('<img src="' + window.loader + '" alt="" />'); // Display a processing icon
    var data = {mediaId: mediaId, rate: rate}; // Create JSON which will be send via Ajax
    $.ajax({ // JQuery Ajax
        type: 'POST',
        url: ROOT_URL + 'ajax/starrating/update/data', // URL to the PHP file which will insert new value in the database
        data: data, // We send the data string
        dataType: 'json',
        timeout: 3000,
        success: function(response) {
            $.cookie("symfonyRatingSystem" + mediaId, "Rated", { expires : 1 }); // Add jQuery Cookie Plugin to use this function
            $('.box' + mediaId).html('<div style="font-size: small; color: green">Thank you for rating</div>'); // Return "Thank you for rating"
            // We update the rating score and number of rates
            $('.resultMedia' + mediaId).html('<div style="font-size: small; color: grey">Rating: ' + response.avg + '/' + numStar + ' (' + response.nbrRate + ' votes)</div>');
            // We recalculate the star bar with new selected stars and unselected stars
            var nbrPixelsInDiv = numStar * starWidth;
            var numEnlightedPX = Math.round(nbrPixelsInDiv * response.avg / numStar);
            $('#group' + mediaId + ' .star_bar').attr('style', 'width:' + nbrPixelsInDiv + 'px; height:' + starWidth + 'px; background: linear-gradient(to right, #ffc600 0%,#ffc600 ' + numEnlightedPX + 'px,#ccc ' + numEnlightedPX + 'px,#ccc 100%);');
            $.each($('#group' + mediaId + ' .star_bar > div'), function () {
                $(this).removeAttr('onmouseover onclick');
            });
        },
        error: function() {
            $('#box').text('Problem');
        }
    });
}

function overStar(mediaId, myRate, numStar) {
    for ( var i = 1; i <= numStar; i++ ) {
        if (i <= myRate) $('#group' + mediaId + ' .star_bar #' + i).attr('class', 'star_hover');
        else $('#group' + mediaId + ' .star_bar #' + i).attr('class', 'star');
    }
}

function outStar(mediaId, myRate, numStar) {
    for ( var i = 1; i <= numStar; i++ ) {
        $('#group' + mediaId + ' .star_bar #' + i).attr('class', 'star');
    }
}

Controller

StarratingsystemController.php

In this first controller I simply select all items from our items table (entity items) and send them to the Twig view Starratingsystem:index.html.twig

<?php
namespace Sim100\TutoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sim100\TutoBundle\Entity\items;

class StarratingsystemController extends Controller
{
    public function indexAction()
    {
        $repository = $this
            ->getDoctrine()
            ->getManager()
            ->getRepository('Sim100TutoBundle:items')
          ;

        $listItems = $repository->findBy(
            array(),                      // Critere
            array('id' => 'asc'),        // Tri
            null,                         // Limite
            null                          // Offset
          );
        
      return $this->render('Sim100TutoBundle:Starratingsystem:index.html.twig', array(
          'listItems' => $listItems,
      ));
      
    }
}

AjaxStarratingsystemController.php

In this second and last controller, I treat data sent by AJAX (by POST). So as you can see in the code:

  1. We get data mediaId and rate
  2. We test if the line exists in our table for this item (or media, whatever)
  3. If the line doesn't exist (means the item is voted for the first time), we make an INSERT INTO. Keep in mind that Doctrine 2 ORM does not support INSERT via DQL or the DQL query builder. To handle inserts in ORM, you always manually instantiate an entity and persist it with the entity manager (check my code below).
  4. If the line exists in the table (means the item has been already voted), we make an UPDATE. We add the rate value and increase the number of vote by 1.
  5. We send back the new data via JSON.
<?php
namespace Sim100\TutoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Cookie;
use Sim100\TutoBundle\Entity\starratingsystem;

class AjaxStarratingsystemController extends Controller
{
    public function updateDataAction(Request $request)
    {
        $mediaId = $request->get('mediaId');
        $rate = $request->get('rate');

        $em = $this->getDoctrine()->getManager();
        
        $rateExists = $em->createQuery('SELECT s.id FROM Sim100TutoBundle:starratingsystem s WHERE s.media = :media')
                ->setParameter('media', $mediaId)
                ->getResult();
        
        if ($rateExists != null) {
            $q = $em->createQuery('UPDATE Sim100TutoBundle:starratingsystem s SET s.rate = s.rate + '.$rate.', s.nbrrate = s.nbrrate + 1 WHERE s.media = ?1')
                ->setParameter(1, $mediaId);
            $q->execute();
        } else {            
            $newRate = new starratingsystem;
            $newRate->setMedia($mediaId);
            $newRate->setRate($rate);
            $newRate->setNbrrate(1);
            $em->persist($newRate);
            $em->flush();
        }
        
        $query = $em->createQuery('SELECT s.rate, s.nbrrate FROM Sim100TutoBundle:starratingsystem s WHERE s.media = :media')
                ->setParameter('media', $mediaId);
        $result = $query->getResult();
        
        $response = new JsonResponse();
        $response->setData(array('avg' => round($result[0]['rate'] / $result[0]['nbrrate'], 2), 'nbrRate' => $result[0]['nbrrate']));
        return $response;
    }
}

Entity

items.php

Table s2_tuto_items: database table which contains items or media. So just add few lines in there. It is very minimalist, only an auto-increment id and a name varchar (255). Add as many fields as you want...

<?php
namespace Sim100\TutoBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * items
 *
 * @ORM\Table(name="s2_tuto_items")
 * @ORM\Entity(repositoryClass="Sim100\TutoBundle\Repository\itemsRepository")
 */
class items
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255)
     */
    private $name;

    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name
     *
     * @param string $name
     *
     * @return items
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }
}

starratingsystem.php

Table s2_tuto_rating. Where we insert rating values for each items or medias. Four fields:

  • id: a standard auto-increment field
  • media: an integer which correspond to the item id of the item table s2_tuto_items
  • rate: the sum of each rate. For example if the first rate is 3 stars out of 5, the field value will be 3. If the second rate is 4 stars out of 5, the new field value will be 7 (3 + 4)...
  • nbrrate: the rate number. Each time someone votes the field is incremented by 1.
<?php
namespace Sim100\TutoBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * countries
 *
 * @ORM\Table(name="s2_tuto_rating")
 * @ORM\Entity(repositoryClass="Sim100\TutoBundle\Repository\starratingsystemRepository")
 */
class starratingsystem
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var integer
     *
     * @ORM\Column(name="media", type="integer")
     */
    private $media;
    
    /**
     * @var integer
     *
     * @ORM\Column(name="rate", type="integer")
     */
    private $rate;
    
    /**
     * @var integer
     *
     * @ORM\Column(name="nbrrate", type="integer")
     */
    private $nbrrate;


    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set media
     *
     * @param integer $media
     *
     * @return starratingsystem
     */
    public function setMedia($media)
    {
        $this->media = $media;

        return $this;
    }

    /**
     * Get media
     *
     * @return integer
     */
    public function getMedia()
    {
        return $this->media;
    }
    
    /**
     * Set rate
     *
     * @param integer $rate
     *
     * @return starratingsystem
     */
    public function setRate($rate)
    {
        $this->rate = $rate;

        return $this;
    }

    /**
     * Get rate
     *
     * @return integer
     */
    public function getRate()
    {
        return $this->rate;
    }
    
    /**
     * Set nbrrate
     *
     * @param integer $nbrrate
     *
     * @return starratingsystem
     */
    public function setNbrrate($nbrrate)
    {
        $this->nbrrate = $nbrrate;

        return $this;
    }

    /**
     * Get nbrrate
     *
     * @return integer
     */
    public function getNbrrate()
    {
        return $this->nbrrate;
    }
}

Resources

config

routing.yml

Two routes:

  • /Starratingsystem:index - which calls the action indexAction of the controller StarratingsystemController.php - this is where we will select all items from the database and send them to the Twig view.
  • /AjaxStarratingsystem:updateData - which calls the action updateData of the controller AjaxStarratingsystem.php - this route will be called by the AJAX when someone make a vote.
tuto_starratingsystem:
    path:     /starratingsystem
    defaults: { _controller: Sim100TutoBundle:Starratingsystem:index }
    
ajax_star_rating_system:
  path:  /ajax/starrating/update/data
  defaults: { _controller: Sim100TutoBundle:AjaxStarratingsystem:updateData }

services.yml

Declaring a service is a key point of this tutorial.

This is how we will create our function calling the star rating bar: {{ starBar(5, item.id, 25)|raw }}

The idea is, for each item sent to the twig view, we call a star rating system bar with 3 arguments:

  • Argument 1: number of stars (5 in our example)
  • Argument 2: the item Id (sent to the twig view)
  • Argument 3: number of pixels of our star (in our case the star .png image is 25x25 pixels)

So, to do that, we need to create a function, what we can call a service in Symfony. Nothing crazy in the code below. As you can see, the service name is StarRatingExtension. There are 2 arguments:

  • @doctrine: simply because in the service, we will need to get some information from the database, we will need to create queries.
  • @request=: we will use request for the cookie. To test if a cookie exists, so we can allow people to vote or not if they have already voted during the last 24 hours.

You may have to change the @request= depending of your Symfony2 version. Please check how to inject the @request into a service (link1). In Symfony 2.4 you can inject the request_stack service (link2).

Notice that services files are places in the directory NameBundle/Twig/NameExtension. So for us it's gona be Sim100/TutoBundle/Twig/StarRatingExtension (file with code is below).

services:

    sim100.twig_extension:
        class: Sim100\TutoBundle\Twig\StarRatingExtension
        arguments: [@doctrine, @request=]
        tags:
            - { name : twig.extension }

views

Starratingsystem

index.html.twig

We simply display items list coming from StarratingsystemController.php -> indexAction.

For each item we add a star rating bar {{ starBar(5, item.id, 25)|raw }}.

<h2>Demo</h2>
    <p>Four items / medias to rate with a 5-stars rating system. You may set up the star number you want in the system.</p>
    
    {% if listItems is empty %}
        <p>No items yet in this category</p>
    {% else %}
        <table class="table table-condensed">
            <tbody>
                <tr>
                {% for item in listItems %}
                    <td>
                        <p><img src="{{ asset('bundles/tuto/images/precise-rating-system/media.png') }}" alt="" /></p>
                        <p class="lead">{{ item.name }}</p>
                        <p>{{ starBar(5, item.id, 25)|raw }}</p>
                    </td>
                {% endfor %}
                </tr>
            </tbody>
        </table>
    {% endif %}

Twig

StarRatingExtension.php

The service I explained above. Check the public function myStarBar. We call and create a star bar for each item displayed. Each star bar has the correct vote value and vote number.

<?php
namespace Sim100\TutoBundle\Twig;
use Symfony\Bridge\Doctrine\RegistryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Cookie;

class StarRatingExtension extends \Twig_Extension
{
    protected $doctrine;
    private $request;

    public function __construct(RegistryInterface $doctrine, Request $request)
    {
        $this->doctrine = $doctrine;
        $this->request = $request;
    }
        
    public function getFunctions() {
      return array(
           'starBar' => new \Twig_Function_Method($this, 'myStarBar'),
      );
    }
    
    public function myStarBar($numStar, $mediaId, $starWidth) {

        $cookies = $this->request->cookies->get('symfonyRatingSystem'.$mediaId);
        
        $nbrPixelsInDiv = $numStar * $starWidth; // Calculate the DIV width in pixel

        $query = $this->doctrine->getRepository('Sim100TutoBundle:starratingsystem')->findOneBy(array('media' => $mediaId));

        if (isset($query)) {
        $average = round($query->getRate()/$query->getNbrrate(), 2);
        $nbrRate = $query->getNbrrate();
        } else {
            $average = 0;
            $nbrRate = 0;
        }

        //num of pixel to colorize (in yellow)
        $numEnlightedPX = round($nbrPixelsInDiv * $average / $numStar, 0);

        $getJSON = array('numStar' => $numStar, 'mediaId' => $mediaId); // We create a JSON with the number of stars and the media ID
        $getJSON = json_encode($getJSON);

        $starBar = '<div id="group'.$mediaId.'">';
        $starBar .= '<div class="star_bar" style="width:'.$nbrPixelsInDiv.'px; height:'.$starWidth.'px; background: linear-gradient(to right, #ffc600 0px,#ffc600 '.$numEnlightedPX.'px,#ccc '.$numEnlightedPX.'px,#ccc '.$nbrPixelsInDiv.'px);" rel='.$getJSON.'>';
        for ($i=1; $i<=$numStar; $i++) {
            $starBar .= '<div title="'.$i.'/'.$numStar.'" id="'.$i.'" class="star"';
            if( !$cookies )
                    $starBar .= ' onmouseover="overStar('.$mediaId.', '.$i.', '.$numStar.');" onmouseout="outStar('.$mediaId.', '.$i.', '.$numStar.');" onclick="rateMedia('.$mediaId.', '.$i.', '.$numStar.', '.$starWidth.');"';
            $starBar .= '></div>';
        }
        $starBar .= '</div>';
        $starBar .= '<div class="resultMedia'.$mediaId.'" style="font-size: small; color: grey">'; // We show the rate score and number of rates
        if (!isset($query)) $starBar .= 'Not rated yet';
        else $starBar .= 'Rating: ' . $average . '/' . $numStar . ' (' . $nbrRate . ' votes)';
        $starBar .= '</div>';
        $starBar .= '<div class="box'.$mediaId.'"></div>';
        $starBar .= '</div>';

        return $starBar;
   }
   
   public function getName()
    {
        return 'StarRating_extension';
    }
}

Help

Need help? Please go to this page and write a comment. Keep in mind that this tutorial is not about how Symfony2 works. You are supposed to know that :).


© 2017 tipocode.com, all rights reserved.