Bagian 5 dari seri

Hasil Nilai Preferensi dan Ranking TOPSIS Laravel

Diterbitkan 21 September 2026

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

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\TopsisService;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;

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

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

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

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

    public function calculate(TopsisService $topsis)
    {
        $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 = $topsis->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 TOPSIS dan ranking berhasil dilakukan.'
            );
    }

    private function buildDetail(
        TopsisService $topsis,
        $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 = $topsis->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 Hasil

Pada navbar:

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

View Hasil Ranking

Buat:

resources/views/hasil/index.blade.php

Isi:

@extends('layouts.app')

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

@section('content')
<div class="d-flex justify-content-between align-items-center mb-3">
    <div>
        <h2 class="h4 mb-1">Hasil Ranking TOPSIS</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 TOPSIS
        </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. Pembagi Normalisasi</h2>

<div class="table-responsive mb-4">
    <table class="table table-bordered">
        <thead>
        <tr>
            <th>Kriteria</th>
            <th>Pembagi</th>
        </tr>
        </thead>

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

<h2 class="h5">4. 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">5. Matriks Normalisasi Terbobot</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['weighted'][$itemAlternatif->id][$itemKriteria->id],
                            6
                        ) }}
                    </td>
                @endforeach
            </tr>
        @endforeach
        </tbody>
    </table>
</div>

<h2 class="h5">6. Solusi Ideal Positif dan Negatif</h2>

<div class="table-responsive mb-4">
    <table class="table table-bordered">
        <thead>
        <tr>
            <th>Kriteria</th>
            <th>Atribut</th>
            <th>A+</th>
            <th>A-</th>
        </tr>
        </thead>

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

<h2 class="h5">7. Jarak dan Nilai Preferensi</h2>

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

        <tbody>
        @foreach($alternatif as $itemAlternatif)
            <tr>
                <td>{{ $itemAlternatif->kode }} - {{ $itemAlternatif->nama }}</td>
                <td>
                    {{ number_format(
                        $detail['distancePositive'][$itemAlternatif->id],
                        6
                    ) }}
                </td>
                <td>
                    {{ number_format(
                        $detail['distanceNegative'][$itemAlternatif->id],
                        6
                    ) }}
                </td>
                <td>
                    {{ number_format(
                        $detail['preferences'][$itemAlternatif->id],
                        6
                    ) }}
                </td>
            </tr>
        @endforeach
        </tbody>
    </table>
</div>

@endif
@endsection

Route Final

Pastikan routes/web.php berisi seluruh route berikut:

<?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

Di dalam navbar resources/views/layouts/app.blade.php, gunakan menu:

@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

File Final yang Harus Ada

app/
├── Http/Controllers/
│   ├── AuthController.php
│   ├── DashboardController.php
│   ├── KriteriaController.php
│   ├── AlternatifController.php
│   ├── PenilaianController.php
│   └── HasilController.php
├── Models/
│   ├── Kriteria.php
│   ├── Alternatif.php
│   └── Penilaian.php
└── Services/
    └── TopsisService.php

database/
├── migrations/
└── seeders/
    └── TopsisSeeder.php

resources/views/
├── auth/login.blade.php
├── dashboard.blade.php
├── layouts/app.blade.php
├── kriteria/
├── alternatif/
├── penilaian/index.blade.php
└── hasil/index.blade.php

routes/web.php

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

Pengujian Akhir

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

  • login dan logout berjalan;
  • CRUD kriteria berjalan;
  • bobot dan atribut kriteria tersimpan;
  • CRUD alternatif berjalan;
  • nilai matriks keputusan dapat disimpan;
  • data penilaian lengkap sebelum dihitung;
  • bobot dinormalisasi;
  • normalisasi matriks berhasil;
  • matriks terbobot berhasil;
  • solusi ideal positif benar berdasarkan benefit/cost;
  • solusi ideal negatif benar berdasarkan benefit/cost;
  • D+ dan D- dihitung;
  • matriks keputusan tampil;
  • bobot normalisasi tampil;
  • pembagi normalisasi tampil;
  • matriks normalisasi tampil;
  • matriks normalisasi terbobot tampil;
  • solusi ideal A+ dan A- tampil;
  • D+ dan D- tampil;
  • nilai preferensi berada pada rentang 0–1;
  • ranking diurutkan dari nilai preferensi terbesar;
  • nilai preferensi tersimpan;
  • ranking tersimpan;
  • nilai yang sama menghasilkan ranking yang sama;
  • membuka halaman hasil dengan GET tidak menghitung ulang data;
  • perhitungan dijalankan melalui tombol POST.

Selesai

Aplikasi TOPSIS Laravel sekarang memiliki alur lengkap:

Login
↓
Kriteria
↓
Alternatif
↓
Penilaian
↓
Matriks Keputusan
↓
Normalisasi
↓
Normalisasi Terbobot
↓
Solusi Ideal
↓
Jarak
↓
Nilai Preferensi
↓
Ranking

Dengan pemisahan perhitungan pada TopsisService, struktur aplikasi lebih mudah dikembangkan dan logika TOPSIS tidak bercampur dengan controller maupun view.

Tutorial Topsis Laravel Hitung Hasil
Tutorial Topsis Laravel Hitung Detail

Kembali ke Tutorial TOPSIS Laravel