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
GETtidak 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.


Kembali ke Tutorial TOPSIS Laravel




