5 Star Rating system with jQuery AJAX in CodeIgniter 4

The 5 star rating system allows users to rate articles on a scale of 1 to 5.

It is a popular way for visitors to merchant websites to rate the quality of products and services. It is also commonly used to rate the quality of user-generated content, such as reviews and comments.

In this tutorial, I am creating a 5 star rating system in CodeIgniter 4 where the ratings will be stored in a database and the average rating will be displayed with the article. Users can rate an article and based on the rating the average rating gets updated.

5 Star rating system with jQuery AJAX in CodeIgniter 4


Contents

  1. Database configuration
  2. Enable CSRF
  3. Create Table
  4. Model
  5. Routes
  6. Controller
  7. View
  8. Demo
  9. Conclusion

1. Database configuration

  • Open .env file which is available at the project root.

NOTE – If dot (.) not added at the start then rename the file to .env.

  • Remove # from start of database.default.hostname, database.default.database, database.default.username, database.default.password, database.default.DBDriver, database.default.DBPrefix, and database.default.port.
  • Update the configuration and save it.
database.default.hostname = 127.0.0.1
database.default.database = codeigniterdb
database.default.username = root
database.default.password = root
database.default.DBDriver = MySQLi
database.default.DBPrefix =
database.default.port = 3306

2. Enable CSRF

  • Again open .env file.
  • Remove # from the start of the security.tokenName,security.headerName, security.cookieName, security.expires,and security.regenerate.
  • I update the security.tokenName value with 'csrf_hash_name'. With this name read CSRF hash. You can update it with any other value.
  • If you don’t want to regenerate CSRF hash after each request then set security.regenerate = false.
security.tokenName = 'csrf_hash_name' 
security.headerName = 'X-CSRF-TOKEN' 
security.cookieName = 'csrf_cookie_name' 
security.expires = 7200 
security.regenerate = true
  • Open app/Config/Filters.php file.
  • Uncomment 'csrf' in 'before' if commented.
// Always applied before every request
public $globals = [
    'before' => [
       // 'honeypot',
       'csrf',
       // 'invalidchars',
    ],
    'after' => [
       'toolbar',
       // 'honeypot',
       // 'secureheaders',
    ],
];

3. Create Table

I am creating 2 tables using migration –

  • posts – Store post records.
  • post_ratings – Store user rating on a post.

posts Table –

php spark migrate:create create_posts_table
  • Now, navigate to app/Database/Migrations/ folder from the project root.
  • Find a PHP file that ends with CreatePostsTable open it.
  • Define the table structure in the up() method.
  • Using the down() method delete posts table that calls when undoing migration.
<?php

namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class CreatePostsTable extends Migration
{
     public function up(){
          $this->forge->addField([
               'id' => [
                     'type' => 'INT',
                     'constraint' => 10,
                     'unsigned' => true,
                     'auto_increment' => true,
               ],
               'title' => [
                   'type' => 'VARCHAR',
                   'constraint' => '100',
               ],
               'description' => [
                    'type' => 'TEXT',
                    'null' => true,
               ],
               'link' => [
                    'type' => 'VARCHAR',
                    'constraint' => '255',
               ],
          ]); 
          $this->forge->addKey('id', true); 
          $this->forge->createTable('posts'); 
     } 
     
     public function down(){ 
          $this->forge->dropTable('posts'); 
     } 
}

post_ratings Table –

php spark migrate:create create_post_ratings_table
  • Again, navigate to the app/Database/Migrations/ folder from the project root.
  • Find a PHP file that ends with CreatePostRatingsTable open it.
  • Define the table structure in the up() method. Added foreign key on post_id field. I didn’t add a foreign key on user_id but you can add it will implementing.
  • Using the down() method delete post_ratings table that calls when undoing migration.
<?php

namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class CreatePostRatingsTable extends Migration
{
     public function up(){
           $this->forge->addField([
                'id' => [
                      'type' => 'INT',
                      'constraint' => 10,
                      'unsigned' => true,
                      'auto_increment' => true,
                ],
                'user_id' => [
                      'type' => 'INT',
                      'constraint' => 10,
                      'unsigned' => true,
                ],
                'post_id' => [
                      'type' => 'INT',
                      'constraint' => 10,
                      'unsigned' => true,
                ],
                'rating' => [
                      'type' => 'VARCHAR',
                      'constraint' => 10
                ]
           ]);

           $this->forge->addKey('id', true);
           $this->forge->addForeignKey('post_id', 'posts', 'id', 'CASCADE', 'CASCADE');
           $this->forge->createTable('post_ratings');

     }

     public function down(){
           $this->forge->dropTable('post_ratings');
     }
}

  • Run the migration –
php spark migrate

4. Model

Create 2 Models –

  • Posts
  • PostRatings

Posts Model –

  • Create Posts Model –
php spark make:model Posts
  • Open app/Models/Posts.php file.
  • In $allowedFields Array specify field names – ['title','description','link'] that can be set during insert and update.

Completed Code

<?php

namespace App\Models;

use CodeIgniter\Model;

class Posts extends Model
{
     protected $DBGroup = 'default';
     protected $table = 'posts';
     protected $primaryKey = 'id';
     protected $useAutoIncrement = true;
     protected $insertID = 0;
     protected $returnType = 'array';
     protected $useSoftDeletes = false;
     protected $protectFields = true;
     protected $allowedFields = ['title','description','link'];

     // Dates
     protected $useTimestamps = false;
     protected $dateFormat = 'datetime';
     protected $createdField = 'created_at';
     protected $updatedField = 'updated_at';
     protected $deletedField = 'deleted_at';

     // Validation
     protected $validationRules = [];
     protected $validationMessages = [];
     protected $skipValidation = false;
     protected $cleanValidationRules = true;

     // Callbacks
     protected $allowCallbacks = true;
     protected $beforeInsert = [];
     protected $afterInsert = [];
     protected $beforeUpdate = [];
     protected $afterUpdate = [];
     protected $beforeFind = [];
     protected $afterFind = [];
     protected $beforeDelete = [];
     protected $afterDelete = [];
}

PostRatings Model –

  • Create PostRatings Model –
php spark make:model PostRatings
  • Open app/Models/PostRatings.php file.
  • In $allowedFields Array specify field names – ['user_id','post_id','rating'] that can be set during insert and update.

Completed Code

<?php

namespace App\Models;

use CodeIgniter\Model;

class PostRatings extends Model
{
     protected $DBGroup = 'default';
     protected $table = 'post_ratings';
     protected $primaryKey = 'id';
     protected $useAutoIncrement = true;
     protected $insertID = 0;
     protected $returnType = 'array';
     protected $useSoftDeletes = false;
     protected $protectFields = true;
     protected $allowedFields = ['user_id','post_id','rating'];

     // Dates
     protected $useTimestamps = false;
     protected $dateFormat = 'datetime';
     protected $createdField = 'created_at';
     protected $updatedField = 'updated_at';
     protected $deletedField = 'deleted_at';

     // Validation
     protected $validationRules = [];
     protected $validationMessages = [];
     protected $skipValidation = false;
     protected $cleanValidationRules = true;

     // Callbacks
     protected $allowCallbacks = true;
     protected $beforeInsert = [];
     protected $afterInsert = [];
     protected $beforeUpdate = [];
     protected $afterUpdate = [];
     protected $beforeFind = [];
     protected $afterFind = [];
     protected $beforeDelete = [];
     protected $afterDelete = [];
}

5. Routes

  • Open app/Config/Routes.php file.
  • Define 2 routes –
    • / – Display records list with rating bar.
    • post/updaterating – Update user post rating.
$routes->get('/', 'PostsController::index');
$routes->get('post/updaterating', 'PostsController::updateRating');

6. Controller

  • Create PostsController Controller –
php spark make:controller PostsController
  • Open app/Controllers/PostsController.php file.
  • Import Posts and PostRatings Model.
  • I created $userid class variable to store user id for rating. You can update its value with logged-in SESSION user id.’

Create 2 methods –

  • index() – Fetch all records from the posts table. Loop on the fetched records to get user rating on the post_ratings table and calculate average rating of the post.

Pass $userrating and $avgrating with post data.

Load index view and pass $data Array.

  • updateRating() – Using this method handle AJAX requests.

Read passed post_id and rating and assign them to the variables. Check if user has any rating on the $post_id or not in the post_ratings table.

If exists then update the rating field value with $rating otherwise insert a new record in the post_ratings table.

Calculate the average rating of the $post_id and assign to $data['avgrating'].

Return $data Array in JSON format.

Completed Code

<?php

namespace App\Controllers;

use App\Controllers\BaseController;
use App\Models\Posts;
use App\Models\PostRatings;

class PostsController extends BaseController
{
      public $userid = 2;// Replace this value with your user SESSION variable

      public function index(){

            $postsObj = new Posts();
            $postRatingsObj = new PostRatings();

            // Fetch all posts
            $postData = $postsObj->select('*')->findAll();
            foreach($postData as $key=>$post){

                  ## User Rating
                  $postratingData = $postRatingsObj->select('rating')
                                    ->where('post_id',$post['id'])
                                    ->where('user_id',$this->userid)
                                    ->find();
                  $userrating = 0;
                  if(!empty($postratingData)){
                        $userrating = $postratingData[0]['rating'];
                  }

                  ## Post average rating 
                  $postratingData = $postRatingsObj->select('ROUND(AVG(rating),1) as averageRating')
                                    ->where('post_id',$post['id'])
                                    ->find();

                  $avgrating = $postratingData[0]['averageRating'];

                  if($avgrating == ''){
                        $avgrating = 0;
                  }

                  $postData[$key]['userrating'] = $userrating;
                  $postData[$key]['avgrating'] = $avgrating;
            }

            $data['posts'] = $postData;

            return view('index',$data); 
      }

      public function updateRating(){
            $request = service('request'); 
            $getData = $request->getGet();

            $post_id = $getData['post_id'];
            $rating = $getData['rating'];

            $postRatingsObj = new PostRatings();

            // Check user rating
            $postratingData = $postRatingsObj->select('id')
                  ->where('post_id',$post_id)
                  ->where('user_id',$this->userid)
                  ->find();

            if(!empty($postratingData)){

                  ## Update user rating
                  $postrating_id = $postratingData[0]['id'];

                  $postRatingsObj->set('rating', $rating);
                  $postRatingsObj->where('id', $postrating_id);
                  $postRatingsObj->update();
            }else{

                  ## Insert user rating
                  $insdata = [
                       'user_id' => $this->userid,
                       'post_id' => $post_id,
                       'rating' => $rating
                  ];

                  $postRatingsObj->insert($insdata);
            }

            // Calculate average rating
            $postratingData = $postRatingsObj->select('ROUND(AVG(rating),1) as averageRating')
                              ->where('post_id',$post_id)
                              ->find();

            $avgrating = $postratingData[0]['averageRating'];

            if($rating == ''){
                  $avgrating = 0;
            }

            $data['avgrating'] = $avgrating;

            return $this->response->setJSON($data);
      }

}

7. View

  • Create index.php file in app/Views/ folder.
  • I am using bootstrap-star-rating jQuery plugin for the rating bar. You can download it from here.
  • Extract the zip file in public/ folder.
  • Include Bootstrap, Fontawesome, jQuery and bootstrap-star-rating library –
<!-- CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/fontawesome.min.css">
<link rel="stylesheet" href="<?= base_url('bootstrap-star-rating/css/star-rating.css') ?>" media="all" type="text/css"/>
<link rel="stylesheet" href="<?= base_url('bootstrap-star-rating/themes/krajee-svg/theme.css') ?>" media="all" type="text/css"/>

<!-- Script -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script type="text/javascript" src="<?= base_url('bootstrap-star-rating/js/star-rating.min.js') ?>"></script>
  • Loop on the $posts. Display post data with 5 star rating bar.

Adding Rating bar –

  • For adding rating bar you need to add <input > element with class rating rating-loading and set data attributes.
<input class="rating rating-loading" 
data-post_id="<?= $post['id'] ?>"
data-min="0" 
data-max="5" 
data-step="0.5" 
data-show-clear="false" 
data-show-caption="false" 
data-size="md" 
value="<?= $post['userrating'] ?>" 
>

I have set the following data attributes –

  • data-post_id – Store post id. Use this value in jQuery when the rating is been changed.
  • data-min – Set minimum rating value.
  • data-max – Set maximum rating value.
  • data-step – Set it to 0.5 if you want to also allow half rating otherwise set it to 1 for full rating.
  • data-show-clear – I set it to false to hide clear rating button.
  • data-show-caption – I set it to false to hide the rating label.
  • data-size – Rating bar size. I set it to md but you can change it to xl, lg, sm, and xs.

In value attribute stored the user rating $post['userrating'].

To display the average rating created <span >. It value gets updated using jQuery when rating is been changed.


jQuery –

Define change event on rating class to detect rating change. Assign selected rating in rating variable and also read post_id from the data attribute.

Send AJAX request to post/updaterating. Pass {post_id: post_id,rating:rating} as data. On successful callback update average rating in <span >.

Completed Code

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>5 Star Rating system with jQuery AJAX in CodeIgniter 4</title>

    <style type="text/css">
    .content{
         border: 0px solid black;
         border-radius: 3px;
         padding: 5px;
         margin: 0 auto;
         width: 50%;
    }

    .post{
         border-bottom: 1px solid black;
         padding: 10px;
         margin-top: 10px;
         margin-bottom: 10px;
    }

    .post:last-child{
         border: 0;
    }

    .post h2{
         font-weight: normal;
         font-size: 30px;
    }

    .post a.link{
         text-decoration: none;
         color: black;
    }

    .post-text{
         letter-spacing: 1px;
         font-size: 15px;
         font-family: serif;
         color: gray;
         text-align: justify;
    }
    .post-action{
         margin-top: 15px;
         margin-bottom: 15px;
    }

    </style>

    <!-- CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/fontawesome.min.css">
    <link rel="stylesheet" href="<?= base_url('bootstrap-star-rating/css/star-rating.css') ?>" media="all" type="text/css"/>
    <link rel="stylesheet" href="<?= base_url('bootstrap-star-rating/themes/krajee-svg/theme.css') ?>" media="all" type="text/css"/>

    <!-- Script -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    <script type="text/javascript" src="<?= base_url('bootstrap-star-rating/js/star-rating.min.js') ?>"></script>
</head>
<body>
    <div class="content">

         <?php 
         foreach($posts as $post){
         ?>
              <div class="post">
                   <h2>
                         <a href='<?= $post['link'] ?>' class='link' target='_blank'><?= $post['title'] ?></a>
                   </h2>
                   <div class="post-text">
                         <?= $post['description'] ?>
                   </div>
                   <div class="post-action">

                         <!-- Rating Bar -->
                         <input class="rating rating-loading" 
                              data-post_id="<?= $post['id'] ?>"
                              data-min="0" 
                              data-max="5" 
                              data-step="0.5" 
                              data-show-clear="false" 
                              data-show-caption="false" 
                              data-size="md" 
                              value="<?= $post['userrating'] ?>" 
                         >
                         <div style='clear: both;'></div>

                         <!-- Display average rating -->
                         Average Rating : <span id='avgrating_<?= $post['id'] ?>'><?= $post['avgrating'] ?></span>
                   </div>
              </div>

         <?php 
         }
         ?>

    </div>

    <!-- Script -->
    <script type="text/javascript">
    $(document).ready(function(){

         // Detect Rating change
         $('.rating').on(
              'change', function () {

                    var rating = $(this).val(); // Selected Rating
                    var post_id = $(this).data('post_id');

                    // AJAX request
                    $.ajax({
                          url:"<?=site_url('post/updaterating')?>",
                          data: {post_id: post_id,rating:rating},
                          dataType: 'json',
                          success: function(response){

                                // Update average rating
                                $('#avgrating_'+post_id).text(response.avgrating)
                          }
                    });

              }
         );
    });
    </script>

</body>
</html>

8. Demo

View Demo


9. Conclusion

In the example, I have fixed the userid for rating. While implementing this on your project replace its value with the logged-in user id.

If you found this tutorial helpful then don't forget to share.

Leave a Comment