Bagian 5 dari seri

Hasil Nilai Preferensi dan Ranking SAW Laravel

Diterbitkan 21 September 2026

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');
});

Menu Final

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