In questo tutorial vedremo come aggiungere una votazione di tipo star rating ai prodotti di un e-commerce in Laravel.

Per capire come procedere, dobbiamo considerare un particolare tipo di relazione esistente tra i dati del nostro database. Un prodotto può avere più votazioni. Questo tipo di relazione si chiama one-to-many (uno a molti).

Per prima cosa, dobbiamo creare una nuova migrazione in Artisan specificando anche il nome del modello di riferimento.

 php artisan make:model Review --migration

Definiamo la nostra tabella nel file della migrazione.

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateReviewsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('reviews', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->tinyInteger('vote');
            $table->bigInteger('product_id');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('reviews');
    }
}

vote sarà un numero compreso tra 1 e 5. product_id serve a creare la relazione one-to-many con i prodotti. Il nome di questo campo deve sempre avere come parte iniziale il nome in minuscolo del modello, seguito sempre da _id. In questo modo Laravel può usarlo come foreign key per stabilire la connessione tra la tabella products e la tabella reviews.

A questo punto definiamo il modello per le votazioni.

namespace App;

use Illuminate\Database\Eloquent\Model;

class Review extends Model
{
    protected $table = 'reviews';
    protected $primaryKey = 'id';
    public $incrementing = true;
    protected $fillable = ['vote', 'product_id'];
}

Quindi lanciamo la migrazione:

php artisan migrate

Per abilitare la relazione one-to-many dobbiamo modificare il modello dei prodotti aggiungendo un metodo che avrà lo stesso nome della tabella delle votazione e al cui interno verrà invocato il metodo hasMany() del modello col riferimento al modello Review.

public function reviews()
    {
        return $this->hasMany('App\Review');
    }

Passiamo ora alla view del singolo prodotto. Qui va inserita la struttura per permettere all'utente di votare.

<div class="mt-3 mb-3 d-flex" id="star-rating">
            @for( $i = 0; $i < 5; $i++)
                <span class="star mr-2" data-vote="{{ $i + 1 }}">
                    <i class="fa fa-star"></i>
                </span>
            @endfor
            <button data-product="{{ $product['id'] }}" type="button" id="create-review" class="ml-4 btn btn-outline-primary btn-sm">Rate</button>
            <span class="text-success ml-3 star-rating-msg">Thank you!</span>
        </div>

Ciascuna stella e il pulsante stesso hanno i dati che ci interessano associati tramite attributi di dati.

Quindi dobbiamo creare il metodo AJAX nel nostro controller che andrà a gestire la richiesta.

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Review;

class AjaxController extends Controller
{
    public function createReview(Request $request)
    {
        $product_id = (int) $request->get('product');
        $vote = (int) $request->get('vote');
        $review = new Review([
           'vote' => $vote,
           'product_id' => $product_id
        ]);
        $review->save();

        return response()->json(['saved' => true]);
    }
}    

Nel frontend con jQuery dobbiamo ricordarci che per le richieste POST va aggiunto il token CSRF come header PHP nelle richieste AJAX. Quindi avremo:

$(function() {

    $.ajaxSetup({
        headers: {
            'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
        }
    });

    if( $( "#star-rating" ).length ) {
        $( ".star", "#star-rating" ).click(function () {
            $( this ).addClass( "checked" ).siblings().removeClass( "checked" );
        });
        $( "#create-review" ).click(function() {
            var $btn = $( this );
            $btn.next().css( "opacity", 0 );
            var data = {
                product: $btn.data( "product" ),
                vote: $( "#star-rating .checked" ).data( "vote" )
            };
            $.post( "/ajax/review", data, function ( res ) {
                if( res.saved ) {
                    $btn.next().css( "opacity", 1 );
                }
            });
        });
    }
});    

A questo punto la votazione viene salvata nel database e non ci resta che mostrarla nella view del singolo prodotto. Per far questo, dobbiamo modificare il metodo del controller che gestisce la route del singolo prodotto aggiungendo la logica relativa al numero di voti ed alla media aritmetica.

public function single($slug)
    {
        $product = Product::where('slug', $slug)->first();

        if(is_null($product)) {
            return redirect()->route('home');
        }

        $reviews = $product->reviews;
        $total_reviews = count($reviews);
        $average = 0;

        if($total_reviews > 0) {
            $total_vote = 0;
            foreach($reviews as $review) {
                $total_vote += $review->vote;
            }

            $average = floor($total_vote / $total_reviews );
        }

        return view('single',[
           'title' => $product->title . ' | E-commerce',
           'product' => $product,
            'total_reviews' => $total_reviews,
            'average' => $average
        ]);
    }

Avrete notato l'espressione $reviews = $product->reviews; che corrisponde appunto al metodo Product::reviews() del modello. Qui entra in gioco la relazione one-to-many: Laravel reperisce i dati della tabella reviews usando il modello Review e li rende disponibili come proprietà del modello Product.

Per concludere, nella view dobbiamo mostrare la media dei voti solo se il prodotto ha delle votazioni associate.

<h1>{{ $product['title'] }}</h1>
            @if($total_reviews > 0)
                <div class="mt-3 mb-3 d-flex">
                    @php $unchecked = 5 - $average; @endphp
                    @for($j = 0; $j < $average; $j++)
                        <span class="star checked mr-2">
                            <i class="fa fa-star"></i>
                        </span>
                    @endfor
                    @for($k = 0; $k < $unchecked; $k++)
                        <span class="star mr-2">
                            <i class="fa fa-star"></i>
                        </span>
                    @endfor
                </div>
            @endif

Si tratta in pratica di mostrare prima un numero di stelle corrispondenti alla media delle votazioni e poi le stelle rimanenti.

Conclusione

In questo tutorial abbiamo scoperto come le relazioni tra modelli e collezioni di Laravel si rivelino essere molto utili per implementare feature avanzate.