Tutorial

Hasil Nilai Preferensi dan Ranking SAW Laravel

Menampilkan matriks keputusan, normalisasi, nilai preferensi, dan ranking SAW Laravel serta menyimpan hasil ke database.

Daftar Isi

Bagian terakhir membuat halaman hasil, detail perhitungan, dan ranking SAW.

Buat Controller

php artisan make:controller HasilController

HasilController

app/Http/Controllers/HasilController.php:

<?php

namespace App\Http\Controllers;

use App\Models\Alternatif;
use App\Models\Kriteria;
use App\Models\Penilaian;
use App\Services\SawService;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;

class HasilController extends Controller
{
    public function index(SawService $saw)
    {
        $kriteria = Kriteria::orderBy('kode')->get();
        $alternatif = Alternatif::orderBy('kode')->get();

        $ranking = Alternatif::orderByRaw(
            'ranking IS NULL, ranking ASC'
        )
            ->orderBy('kode')
            ->get();

        $detail = $this->buildDetail(
            $saw,
            $kriteria,
            $alternatif
        );

        return view('hasil.index', compact(
            'ranking',
            'kriteria',
            'alternatif',
            'detail'
        ));
    }

    public function calculate(SawService $saw)
    {
        $kriteria = Kriteria::orderBy('kode')->get();
        $alternatif = Alternatif::orderBy('kode')->get();

        if ($kriteria->isEmpty()) {
            return redirect()
                ->route('hasil.index')
                ->withErrors('Kriteria belum tersedia.');
        }

        if ($alternatif->count() < 2) {
            return redirect()
                ->route('hasil.index')
                ->withErrors('Minimal diperlukan dua alternatif.');
        }

        $penilaian = Penilaian::all();

        $expected =
            $kriteria->count()
            * $alternatif->count();

        if ($penilaian->count() !== $expected) {
            return redirect()
                ->route('hasil.index')
                ->withErrors('Data penilaian belum lengkap.');
        }

        $weights = [];
        $attributes = [];
        $matrix = [];

        $nilaiMap = $penilaian->keyBy(function ($item) {
            return $item->alternatif_id
                . '-'
                . $item->kriteria_id;
        });

        foreach ($kriteria as $itemKriteria) {
            $weights[$itemKriteria->id] =
                (float) $itemKriteria->bobot;

            $attributes[$itemKriteria->id] =
                $itemKriteria->atribut;
        }

        foreach ($alternatif as $itemAlternatif) {
            foreach ($kriteria as $itemKriteria) {
                $key =
                    $itemAlternatif->id
                    . '-'
                    . $itemKriteria->id;

                if (! $nilaiMap->has($key)) {
                    return redirect()
                        ->route('hasil.index')
                        ->withErrors('Data penilaian belum lengkap.');
                }

                $matrix[$itemAlternatif->id]
                    [$itemKriteria->id] =
                    (float) $nilaiMap[$key]->nilai;
            }
        }

        try {
            $hasil = $saw->calculate(
                $matrix,
                $weights,
                $attributes
            );
        } catch (InvalidArgumentException $e) {
            return redirect()
                ->route('hasil.index')
                ->withErrors($e->getMessage());
        }

        $preferences = $hasil['preferences'];

        arsort($preferences);

        DB::transaction(function () use ($preferences) {
            $position = 0;
            $ranking = 0;
            $previousValue = null;
            $epsilon = 0.0000000001;

            foreach ($preferences as $alternatifId => $value) {
                $position++;

                if (
                    $previousValue === null
                    || abs($value - $previousValue) > $epsilon
                ) {
                    $ranking = $position;
                }

                Alternatif::whereKey($alternatifId)->update([
                    'nilai_preferensi' => $value,
                    'ranking' => $ranking,
                ]);

                $previousValue = $value;
            }
        });

        return redirect()
            ->route('hasil.index')
            ->with(
                'success',
                'Perhitungan SAW dan ranking berhasil dilakukan.'
            );
    }

    private function buildDetail(
        SawService $saw,
        $kriteria,
        $alternatif
    ): ?array {
        if ($kriteria->isEmpty() || $alternatif->isEmpty()) {
            return null;
        }

        $penilaian = Penilaian::all();

        $expected =
            $kriteria->count()
            * $alternatif->count();

        if ($penilaian->count() !== $expected) {
            return null;
        }

        $weights = [];
        $attributes = [];
        $matrix = [];

        $nilaiMap = $penilaian->keyBy(function ($item) {
            return $item->alternatif_id
                . '-'
                . $item->kriteria_id;
        });

        foreach ($kriteria as $itemKriteria) {
            $weights[$itemKriteria->id] =
                (float) $itemKriteria->bobot;

            $attributes[$itemKriteria->id] =
                $itemKriteria->atribut;
        }

        foreach ($alternatif as $itemAlternatif) {
            foreach ($kriteria as $itemKriteria) {
                $key =
                    $itemAlternatif->id
                    . '-'
                    . $itemKriteria->id;

                if (! $nilaiMap->has($key)) {
                    return null;
                }

                $matrix[$itemAlternatif->id]
                    [$itemKriteria->id] =
                    (float) $nilaiMap[$key]->nilai;
            }
        }

        try {
            $hasil = $saw->calculate(
                $matrix,
                $weights,
                $attributes
            );
        } catch (InvalidArgumentException) {
            return null;
        }

        $hasil['matrix'] = $matrix;

        return $hasil;
    }
}

Route

Tambahkan pada grup auth:

use App\Http\Controllers\HasilController;

Route::get(
    '/hasil',
    [HasilController::class, 'index']
)->name('hasil.index');

Route::post(
    '/hasil/hitung',
    [HasilController::class, 'calculate']
)->name('hasil.calculate');

Tambahkan Menu

<a
    class="nav-link text-white"
    href="{{ route('hasil.index') }}">
    Hasil
</a>

View Hasil

resources/views/hasil/index.blade.php:

@extends('layouts.app')

@section('title', 'Hasil SAW')

@section('content')
<div class="d-flex justify-content-between align-items-center mb-3">
    <div>
        <h2 class="h4 mb-1">Hasil Ranking SAW</h2>

        <p class="text-muted mb-0">
            Nilai preferensi terbesar mendapat ranking tertinggi.
        </p>
    </div>

    <form method="POST" action="{{ route('hasil.calculate') }}">
        @csrf

        <button class="btn btn-primary">
            Hitung SAW
        </button>
    </form>
</div>

<div class="card border-0 shadow-sm mb-4">
    <div class="card-body">
        <div class="table-responsive">
            <table class="table table-bordered table-striped align-middle mb-0">
                <thead>
                <tr>
                    <th width="100">Ranking</th>
                    <th>Kode</th>
                    <th>Alternatif</th>
                    <th>Nilai Preferensi</th>
                </tr>
                </thead>

                <tbody>
                @forelse($ranking as $item)
                    <tr>
                        <td>
                            @if($item->ranking)
                                <span class="badge text-bg-primary">
                                    {{ $item->ranking }}
                                </span>
                            @else
                                -
                            @endif
                        </td>

                        <td>{{ $item->kode }}</td>
                        <td>{{ $item->nama }}</td>

                        <td>
                            @if($item->nilai_preferensi !== null)
                                {{ number_format(
                                    $item->nilai_preferensi,
                                    6
                                ) }}
                            @else
                                Belum dihitung
                            @endif
                        </td>
                    </tr>
                @empty
                    <tr>
                        <td colspan="4" class="text-center">
                            Belum ada alternatif.
                        </td>
                    </tr>
                @endforelse
                </tbody>
            </table>
        </div>
    </div>
</div>

@if($detail)

<h2 class="h5 mt-4">1. Matriks Keputusan</h2>

<div class="table-responsive mb-4">
    <table class="table table-bordered text-center align-middle">
        <thead>
        <tr>
            <th>Alternatif</th>

            @foreach($kriteria as $itemKriteria)
                <th>
                    {{ $itemKriteria->kode }}
                    <div class="small fw-normal">
                        {{ ucfirst($itemKriteria->atribut) }}
                    </div>
                </th>
            @endforeach
        </tr>
        </thead>

        <tbody>
        @foreach($alternatif as $itemAlternatif)
            <tr>
                <th>{{ $itemAlternatif->kode }}</th>

                @foreach($kriteria as $itemKriteria)
                    <td>
                        {{ number_format(
                            $detail['matrix'][$itemAlternatif->id][$itemKriteria->id],
                            6
                        ) }}
                    </td>
                @endforeach
            </tr>
        @endforeach
        </tbody>
    </table>
</div>

<h2 class="h5">2. Bobot Normalisasi</h2>

<div class="table-responsive mb-4">
    <table class="table table-bordered">
        <thead>
        <tr>
            <th>Kriteria</th>
            <th>Atribut</th>
            <th>Bobot Awal</th>
            <th>Bobot Normalisasi</th>
        </tr>
        </thead>

        <tbody>
        @foreach($kriteria as $itemKriteria)
            <tr>
                <td>{{ $itemKriteria->kode }} - {{ $itemKriteria->nama }}</td>
                <td>{{ ucfirst($itemKriteria->atribut) }}</td>
                <td>{{ $itemKriteria->bobot }}</td>
                <td>
                    {{ number_format(
                        $detail['weights'][$itemKriteria->id],
                        6
                    ) }}
                </td>
            </tr>
        @endforeach
        </tbody>
    </table>
</div>

<h2 class="h5">3. Matriks Normalisasi</h2>

<div class="table-responsive mb-4">
    <table class="table table-bordered text-center align-middle">
        <thead>
        <tr>
            <th>Alternatif</th>

            @foreach($kriteria as $itemKriteria)
                <th>{{ $itemKriteria->kode }}</th>
            @endforeach
        </tr>
        </thead>

        <tbody>
        @foreach($alternatif as $itemAlternatif)
            <tr>
                <th>{{ $itemAlternatif->kode }}</th>

                @foreach($kriteria as $itemKriteria)
                    <td>
                        {{ number_format(
                            $detail['normalized'][$itemAlternatif->id][$itemKriteria->id],
                            6
                        ) }}
                    </td>
                @endforeach
            </tr>
        @endforeach
        </tbody>
    </table>
</div>

<h2 class="h5">4. Nilai Preferensi</h2>

<div class="table-responsive mb-4">
    <table class="table table-bordered">
        <thead>
        <tr>
            <th>Alternatif</th>
            <th>Nilai Preferensi</th>
        </tr>
        </thead>

        <tbody>
        @foreach($alternatif as $itemAlternatif)
            <tr>
                <td>
                    {{ $itemAlternatif->kode }}
                    -
                    {{ $itemAlternatif->nama }}
                </td>

                <td>
                    {{ number_format(
                        $detail['preferences'][$itemAlternatif->id],
                        6
                    ) }}
                </td>
            </tr>
        @endforeach
        </tbody>
    </table>
</div>

@endif
@endsection

Route Final

Pastikan routes/web.php berisi:

<?php

use App\Http\Controllers\AlternatifController;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\HasilController;
use App\Http\Controllers\KriteriaController;
use App\Http\Controllers\PenilaianController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return redirect()->route('dashboard');
});

Route::middleware('guest')->group(function () {
    Route::get('/login', [AuthController::class, 'create'])
        ->name('login');

    Route::post('/login', [AuthController::class, 'store'])
        ->middleware('throttle:5,1')
        ->name('login.store');
});

Route::middleware('auth')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index'])
        ->name('dashboard');

    Route::resource('kriteria', KriteriaController::class)
        ->except('show')
        ->parameters(['kriteria' => 'kriteria']);

    Route::resource('alternatif', AlternatifController::class)
        ->except('show');

    Route::get('/penilaian', [PenilaianController::class, 'index'])
        ->name('penilaian.index');

    Route::post('/penilaian', [PenilaianController::class, 'store'])
        ->name('penilaian.store');

    Route::get('/hasil', [HasilController::class, 'index'])
        ->name('hasil.index');

    Route::post('/hasil/hitung', [HasilController::class, 'calculate'])
        ->name('hasil.calculate');

    Route::post('/logout', [AuthController::class, 'destroy'])
        ->name('logout');
});

Pada navbar:

@auth
<div class="d-flex align-items-center gap-3">
    <a class="nav-link text-white"
       href="{{ route('kriteria.index') }}">
        Kriteria
    </a>

    <a class="nav-link text-white"
       href="{{ route('alternatif.index') }}">
        Alternatif
    </a>

    <a class="nav-link text-white"
       href="{{ route('penilaian.index') }}">
        Penilaian
    </a>

    <a class="nav-link text-white"
       href="{{ route('hasil.index') }}">
        Hasil
    </a>

    <span class="text-white">
        {{ auth()->user()->name }}
    </span>

    <form method="POST" action="{{ route('logout') }}">
        @csrf

        <button class="btn btn-outline-light btn-sm">
            Logout
        </button>
    </form>
</div>
@endauth

Jalankan Project

Jalankan:

php artisan migrate:fresh --seed
php artisan serve

Buka:

http://127.0.0.1:8000

Login:

Email    : admin@example.com
Password : admin12345
Tutorial Saw Laravel Hitung Hasil
Tutorial Saw Laravel Hitung Detail

Pengujian Akhir

Gunakan data contoh dari tutorial, klik Hitung SAW, lalu pastikan:

  • matriks keputusan tampil;
  • bobot normalisasi tampil;
  • matriks normalisasi tampil;
  • nilai preferensi tampil;
  • ranking tersimpan;
  • ranking diurutkan dari nilai preferensi terbesar.

Selesai

Aplikasi SAW Laravel sudah memiliki alur lengkap:

Login
↓
Kriteria
↓
Alternatif
↓
Penilaian
↓
Normalisasi
↓
Nilai Preferensi
↓
Ranking
Kembali ke Tutorial SAW Laravel

Tulis Komentar

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *