Sistem terintegrasi berbasis Laravel Modular Monolith yang terdiri dari empat aplikasi akademik dalam satu proyek terpusat.
π Documentation β’ π Quick Start β’ ποΈ Architecture β’ β‘ Performance β’ π€ Contributing
- Contributor
- Overview
- System Modules
- System Architecture
- Project Structure
- Quick Start
- Database Convention
- Migration Guide
- Database Seeding
- Essential Commands
- User Role System
- Development Rules
- Performance Configuration
- Redis Setup
- Laravel Telescope (Opsional)
- Troubleshooting
- Future Roadmap
- Project Lead: Bimo Kusumo Putro Wicaksono
- Bank Soal: Dzaki Eka Atmaja, Evan Adkara Christian P, Nabil Bintang Ardiansyah P.
- Capstone + TA: Ananda Prida Yusuf S, Fayyadh Muhammad Habibie, Muhammad Riza Saputra
- E-Office: Andhinee Clarisaa Tanasale, Cetta Masinda Amany, Elvina Nasywa Ariyani
- Manajemen Kemahasiswaan + KP: Devarlo Rahadyan Razan, Muhammad Reswara Suryawan, Surya Hari Putra, Syahbana Hatab
Web Akademik Terintegrasi Teknik Komputer adalah platform akademik terpusat yang dibangun dengan Laravel Modular Architecture dan Supabase sebagai database backend, menggabungkan empat sistem utama dalam satu codebase untuk efisiensi dan konsistensi data.
- π― Modular Architecture - Setiap modul independen namun terintegrasi
- π’ Supabase Backend - PostgreSQL hosting dengan realtime features
- ποΈ Single Database - Satu database PostgreSQL terpusat
- π Role-Based Access Control - 4 level user roles
- β‘ Optimized Performance - Redis caching + persistent DB connection
- π Scalable Design - Mudah dikembangkan ke microservices
|
Manajemen topik, bimbingan, workflow, dan evaluasi tugas akhir |
Sistem manajemen bank soal dan ujian online |
Kegiatan, organisasi, dan administrasi mahasiswa |
Surat menyurat dan manajemen dokumen internal |
βββββββββββββββββββββββββββββββββββββββββββββββ
β Client (Browser/Mobile) β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββββββ
β Laravel Application (Monolith) β
β βββββββββββββββββββββββββββββββββββββββββ β
β β Core (Global Layer) β β
β β β’ users β’ students β’ lecturers β β
β βββββββββββββββββββββββββββββββββββββββββ β
β β
β βββββββββββββββββββββββββββββββββββββββββ β
β β Module Layer β β
β β ββββββββββββ ββββββββββββ β β
β β β Capstone β β BankSoal β β β
β β ββββββββββββ ββββββββββββ β β
β β ββββββββββββ ββββββββββββ β β
β β βKemahasis-β β EOffice β β β
β β β waan β β β β β
β β ββββββββββββ ββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β
βββββββββββββ΄ββββββββββββ
βΌ βΌ
βββββββββββββββ ββββββββββββββββββββββββ
β π΄ Redis β β π’ Supabase β
β (Cache + β β (PostgreSQL Database) β
β Session) β β β
βββββββββββββββ ββββββββββββββββββββββββ
WebsiteTekkom/
βββ app/
β βββ Http/
β β βββ Middleware/
β β βββ CheckRole.php # Role-based access dengan Redis cache
β βββ Models/
β β βββ User.php # getCachedRoles(), cacheUserData()
β βββ Providers/
β βββ AppServiceProvider.php # Cached Eloquent User Provider
βββ config/
β βββ auth.php # cached-eloquent driver
β βββ database.php # PDO persistent connection
β βββ cache.php
βββ database/
β βββ migrations/
β βββ seeders/
β βββ DatabaseSeeder.php # Global seeder entry point
βββ Modules/
β βββ Capstone/
β β βββ Database/
β β βββ Seeders/
β βββ BankSoal/
β β βββ Database/
β β βββ Seeders/
β βββ Kemahasiswaan/
β β βββ Database/
β β βββ Seeders/
β βββ EOffice/
β βββ Database/
β βββ Seeders/
βββ routes/
βββ .env.example
βββ composer.json
βββ README.md
- Laravel >= 12
- PHP >= 8.2.12
- Composer >= 2.9.5
- Supabase Account (or PostgreSQL >= 14)
- Node.js >= 18 (optional, for frontend assets)
- Redis (Memurai untuk Windows, atau Redis untuk Linux/Mac)
git clone https://github.com/bimo3058/WebsiteTekkom.git
cd WebsiteTekkomcomposer install
composer require predis/prediscp .env.example .env
php artisan key:generateEdit .env:
# Database - Supabase Singapore (ap-southeast-1)
DB_CONNECTION=pgsql
DB_HOST=aws-0-ap-southeast-1.pooler.supabase.com
DB_PORT=6543
DB_DATABASE=postgres
DB_USERNAME=postgres.your-project-ref
DB_PASSWORD=your-supabase-password
# Redis Cache
CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_CLIENT=predis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379php artisan migrateJalankan di Supabase SQL Editor untuk optimasi query:
-- Index untuk login query
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email_active
ON users(email) WHERE deleted_at IS NULL;
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_id_active
ON users(id) WHERE deleted_at IS NULL;
-- Index untuk roles lookup
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_user_roles_user_id
ON user_roles(user_id);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_roles_name
ON roles(name);
-- Index untuk capstone
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_capstone_groups_period_status
ON capstone_groups(period_id, status) WHERE deleted_at IS NULL;
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_capstone_titles_status
ON capstone_titles(status, approved_by_admin) WHERE deleted_at IS NULL;
-- Index untuk bank soal
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_pertanyaan_mk_status
ON bs_pertanyaan(mk_id, status);Edit config/auth.php:
'providers' => [
'users' => [
'driver' => 'cached-eloquent', // pakai cached provider
'model' => App\Models\User::class,
],
],# Start Redis dulu (pastikan Memurai jalan di Windows)
# Lalu jalankan Laravel
composer run devVisit: http://localhost:8000
Sistem ini telah dioptimasi dari response time ~3200ms β <1000ms melalui beberapa teknik berikut.
| Request | Sebelum | Sesudah | Improvement |
|---|---|---|---|
| POST /login | ~3200ms | ~400ms | 87% faster |
| GET /dashboard | ~2580ms | <1000ms | ~60% faster |
| GET /superadmin/dashboard | ~2580ms | <1000ms | ~60% faster |
File: config/database.php
Tambahkan PDO::ATTR_PERSISTENT di konfigurasi pgsql agar koneksi ke Supabase di-reuse antar request, menghilangkan overhead TCP handshake (~500ms) di setiap request.
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'require',
'options' => [
PDO::ATTR_PERSISTENT => true, // reuse koneksi antar request
PDO::ATTR_TIMEOUT => 10,
],
],File: app/Providers/AppServiceProvider.php
Laravel memanggil retrieveById() di setiap request untuk re-authenticate user dari session. Override ini mengambil user dari Redis (~1ms) bukan DB (~1150ms).
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
public function register(): void
{
Auth::resolved(function ($auth) {
$auth->provider('cached-eloquent', function ($app, array $config) {
return new class($app['hash'], $config['model']) extends EloquentUserProvider {
public function retrieveById($identifier): ?Authenticatable
{
$cacheKey = "user:{$identifier}:data";
$cached = Cache::get($cacheKey);
if ($cached) {
$model = $this->createModel();
return $model->newFromBuilder($cached);
}
$user = parent::retrieveById($identifier);
if ($user) {
Cache::put($cacheKey, $user->withoutRelations()->toArray(), now()->addHours(8));
}
return $user;
}
};
});
});
// ... singleton registrations
}Daftarkan di config/auth.php:
'providers' => [
'users' => [
'driver' => 'cached-eloquent',
'model' => App\Models\User::class,
],
],File: app/Models/User.php
hasRole(), hasAnyRole(), hasAllRoles() mengambil roles dari Redis cache bukan query DB setiap kali dipanggil.
use Illuminate\Support\Facades\Cache;
// Relasi dengan select kolom spesifik
public function roles()
{
return $this->belongsToMany(Role::class, 'user_roles')
->select('roles.id', 'roles.name', 'roles.module');
}
// Semua role helper pakai getCachedRoles()
public function hasRole(string $roleName, ?string $module = null): bool
{
return $this->getCachedRoles()
->when($module, fn($c) => $c->where('module', $module))
->contains('name', strtolower($roleName));
}
protected function getCachedRoles(): \Illuminate\Support\Collection
{
if ($this->relationLoaded('roles')) {
return collect($this->roles);
}
$cached = Cache::get("user:{$this->id}:roles");
if ($cached) {
return collect($cached);
}
$roles = $this->roles()->get();
Cache::put("user:{$this->id}:roles", $roles->toArray(), now()->addHours(8));
return $roles;
}
// Cache semua data user setelah login
public function cacheUserData(): void
{
Cache::put(
"user:{$this->id}:data",
$this->makeVisible(['remember_token'])->withoutRelations()->toArray(),
now()->addHours(8)
);
}
// Hapus cache saat logout atau data user berubah
public function clearUserCache(): void
{
Cache::forget("user:{$this->id}:data");
Cache::forget("user:{$this->id}:roles");
}File: app/Http/Controllers/Auth/AuthenticatedSessionController.php
Simpan user data + roles ke Redis segera setelah login berhasil, sehingga request berikutnya (dashboard) tidak perlu query DB sama sekali.
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
$user = auth()->user();
$userRoles = $user->roles()->get();
// Cache sekaligus setelah login
$user->cacheUserData();
Cache::put("user:{$user->id}:roles", $userRoles->toArray(), now()->addHours(8));
$roleNames = $userRoles->pluck('name');
if ($roleNames->intersect(['superadmin', 'admin'])->isNotEmpty()) {
return redirect()->intended(route('superadmin.dashboard'));
}
if ($roleNames->contains('dosen')) {
return redirect()->intended(route('dashboard'));
}
return redirect()->intended(route('dashboard'));
}
public function destroy(Request $request): RedirectResponse
{
$user = auth()->user();
Auth::guard('web')->logout();
if ($user) {
$user->clearUserCache(); // hapus cache saat logout
}
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}File: app/Http/Middleware/CheckRole.php
Middleware ini jalan di setiap request. Tanpa cache, tiap halaman akan query roles ke DB. Dengan cache, cukup 1ms dari Redis.
public function handle(Request $request, Closure $next, string $role): Response
{
if (!auth()->check()) {
return redirect('/login');
}
$userId = auth()->id(); // tidak trigger DB query
$cached = Cache::get("user:{$userId}:roles");
if ($cached) {
$userRoles = collect($cached)->pluck('name');
} else {
$rolesCollection = auth()->user()->roles()->get();
Cache::put("user:{$userId}:roles", $rolesCollection->toArray(), now()->addHours(8));
$userRoles = $rolesCollection->pluck('name');
}
$roles = collect(explode('|', $role))->map(fn($r) => strtolower($r));
$hasRole = $roles->some(fn($r) => $userRoles->contains($r));
if (!$hasRole) {
abort(403, 'Unauthorized');
}
return $next($request);
}
β οΈ Penting: Setiap kali update roles user, wajib panggil$user->clearUserCache()supaya perubahan langsung efektif.
Untuk data yang jarang berubah tapi sering dibaca, gunakan pola Cache::remember():
// Contoh di service class
public function getActivePeriod(): ?CapstonePeriod
{
return Cache::remember('capstone:period:active', now()->addHour(), fn() =>
CapstonePeriod::where('is_active', true)->first()
);
}
public function getMataKuliahList(): Collection
{
return Cache::remember('banksoal:mk:all', now()->addDay(), fn() =>
MataKuliah::with('cpls')->orderBy('kode')->get()
);
}Cache key convention:
{modul}:{entity}:{scope}:{id}
user:1:data β data user
user:1:roles β roles user
capstone:period:active
banksoal:mk:all
banksoal:statistik:mk:42
Redis digunakan sebagai cache dan session driver untuk menghindari query DB berulang di setiap request.
1. Install Memurai
Download di memurai.com/get-memurai β install. Memurai otomatis berjalan sebagai Windows Service.
Verifikasi:
memurai-cli ping
# PONG2. Install Predis
composer require predis/predis3. Hapus php_redis.dll dari php.ini
Buka C:\xampp\php\php.ini, comment out jika ada:
;extension=php_redis.dll4. Update .env
CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_CLIENT=predis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=63795. Clear config
php artisan config:clear
php artisan cache:clear6. Verifikasi
php artisan tinker
Cache::put('test', 'redis working!', 60);
Cache::get('test'); // β "redis working!"# Ubuntu/Debian
sudo apt install redis-server
sudo systemctl enable redis-server
sudo systemctl start redis-server
# Mac (Homebrew)
brew install redis
brew services start redis
# Verifikasi
redis-cli ping # PONGLanjutkan dari langkah 2. Install Predis di atas.
Laragon sudah include Redis bawaan β lebih simpel dari setup manual:
- Download di laragon.org
- Install β klik kanan tray icon β centang Redis
- Pindahkan project ke
C:\laragon\www\webtekkom - Update
.envseperti di atas
β οΈ Redis harus jalan sebelum Laravel dijalankan. Karena session disimpan di Redis, kalau Redis mati semua user tidak bisa login.
β οΈ Jalankanphp artisan cache:clearsetelah mengubah data cache untuk menghindari stale data.
Tabel global tanpa prefix:
| Table | Description |
|---|---|
users |
User authentication |
students |
Student data |
lecturers |
Lecturer data |
| Module | Prefix | Example Tables |
|---|---|---|
| π Capstone | capstone_ |
capstone_periods, capstone_topics |
| π Bank Soal | bs_ |
bs_pertanyaan, bs_mata_kuliah |
| π Kemahasiswaan | mk_ |
mk_kegiatan, mk_pengumuman |
| π E-Office | eo_ |
eo_surat, eo_dokumen |
β οΈ IMPORTANT: Semua tabel module WAJIB menggunakan prefix yang sesuai.
php artisan migratephp artisan migrate --path=Modules/Capstone/Database/Migrations
php artisan migrate --path=Modules/BankSoal/Database/Migrations
php artisan migrate --path=Modules/Kemahasiswaan/Database/Migrations
php artisan migrate --path=Modules/EOffice/Database/Migrationsphp artisan migrate:fresh# Global migration
php artisan make:migration create_users_table
# Module-specific migration
php artisan make:migration create_capstone_periods_table --path=Modules/Capstone/Database/MigrationsSeeding digunakan untuk mengisi database dengan data awal (roles, user dummy, data referensi, dll). Proyek ini menggunakan dua lapisan seeder: global di database/seeders/ dan per-modul di Modules/{Nama}/Database/Seeders/.
database/
βββ seeders/
βββ DatabaseSeeder.php β Entry point utama (global)
βββ RoleSeeder.php β Seed semua roles (global + per-modul)
βββ SuperAdminSeeder.php β Seed akun superadmin
βββ UserSeeder.php β Seed dosen, mahasiswa & assign roles
Modules/
βββ Capstone/
β βββ Database/
β βββ Seeders/
β βββ CapstoneSeeder.php β Seeder utama modul Capstone
β βββ ...
βββ BankSoal/
β βββ Database/
β βββ Seeders/
β βββ BankSoalSeeder.php
β βββ ...
βββ Kemahasiswaan/
β βββ Database/
β βββ Seeders/
β βββ KemahasiswaanSeeder.php
β βββ ...
βββ EOffice/
βββ Database/
βββ Seeders/
βββ EOfficeSeeder.php
βββ ...
database/seeders/DatabaseSeeder.php adalah entry point utama. Cukup memanggil RoleSeeder (wajib duluan) lalu UserSeeder yang sudah mencakup semua user dummy beserta assignment role-nya.
β οΈ Urutan wajib dijaga:RoleSeederharus dijalankan sebelumUserSeederkarenaUserSeederakan langsungfirstOrFail()ke tabel roles. Kalau roles belum ada, seeder langsung error.
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
RoleSeeder::class, // WAJIB pertama β UserSeeder bergantung pada ini
UserSeeder::class, // seed dosen, mahasiswa + assign roles
]);
}
}RoleSeeder.php β seed semua roles, termasuk role spesifik per modul seperti gpm untuk Bank Soal:
<?php
namespace Database\Seeders;
use App\Models\Role;
use Illuminate\Database\Seeder;
class RoleSeeder extends Seeder
{
public function run(): void
{
$roles = [
// Global roles
['name' => 'superadmin', 'module' => 'global'],
['name' => 'admin', 'module' => 'global'],
['name' => 'dosen', 'module' => 'global'],
['name' => 'mahasiswa', 'module' => 'global'],
// Module-specific roles
['name' => 'gpm', 'module' => 'bank_soal'],
];
foreach ($roles as $role) {
Role::firstOrCreate(
['name' => $role['name'], 'module' => $role['module']],
$role
);
}
}
}SuperAdminSeeder.php β seed akun superadmin saja, bisa dijalankan terpisah jika dibutuhkan:
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class SuperAdminSeeder extends Seeder
{
public function run(): void
{
User::updateOrCreate(
['email' => 'superadmin@kampus.ac.id'],
[
'name' => 'Super Admin',
'password' => Hash::make('password123'),
]
);
}
}π‘
SuperAdminSeedertidak dipanggil dariDatabaseSeederkarena pembuatan akun superadmin sudah ditangani di dalamUserSeeder. Seeder ini tersedia sebagai utilitas terpisah β berguna jika akun superadmin terhapus dan perlu di-restore cepat tanpa harus seed ulang semua data.
UserSeeder.php β seed semua user (dosen, dosen+gpm, mahasiswa) sekaligus membuat record terkait di tabel lecturers dan students, serta assign roles:
<?php
namespace Database\Seeders;
use App\Models\Lecturer;
use App\Models\Role;
use App\Models\Student;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class UserSeeder extends Seeder
{
public function run(): void
{
// Load semua role di awal β langsung error kalau RoleSeeder belum dijalankan
$roles = [
'superadmin' => Role::where('name', 'superadmin')->where('module', 'global')->firstOrFail(),
'dosen' => Role::where('name', 'dosen')->where('module', 'global')->firstOrFail(),
'mahasiswa' => Role::where('name', 'mahasiswa')->where('module', 'global')->firstOrFail(),
'gpm' => Role::where('name', 'gpm')->where('module', 'bank_soal')->firstOrFail(),
];
// 1. SUPERADMIN
$superadmin = User::firstOrCreate(
['email' => 'superadmin@kampus.ac.id'],
[
'external_id' => 'EXT-SUPERADMIN-001',
'name' => 'Super Admin',
'password' => Hash::make('password'),
]
);
$superadmin->roles()->syncWithoutDetaching([$roles['superadmin']->id]);
// 2. DOSEN
$dosenUsers = [
[
'external_id' => 'EXT-DSN-001',
'name' => 'Dr. Budi Santoso',
'email' => 'budi.santoso@kampus.ac.id',
'employee_number' => 'NIP-2001-001',
],
[
'external_id' => 'EXT-DSN-002',
'name' => 'Dr. Siti Rahayu',
'email' => 'siti.rahayu@kampus.ac.id',
'employee_number' => 'NIP-2001-002',
],
];
foreach ($dosenUsers as $data) {
$user = User::firstOrCreate(
['email' => $data['email']],
[
'external_id' => $data['external_id'],
'name' => $data['name'],
'password' => Hash::make('password'),
]
);
$user->roles()->syncWithoutDetaching([$roles['dosen']->id]);
Lecturer::firstOrCreate(
['user_id' => $user->id],
['employee_number' => $data['employee_number']]
);
}
// 3. DOSEN + GPM (punya dua role sekaligus)
$dosenGpmUsers = [
[
'external_id' => 'EXT-GPM-001',
'name' => 'Prof. Ahmad Fauzi',
'email' => 'ahmad.fauzi@kampus.ac.id',
'employee_number' => 'NIP-2001-003',
],
[
'external_id' => 'EXT-GPM-002',
'name' => 'Prof. Dewi Lestari',
'email' => 'dewi.lestari@kampus.ac.id',
'employee_number' => 'NIP-2001-004',
],
];
foreach ($dosenGpmUsers as $data) {
$user = User::firstOrCreate(
['email' => $data['email']],
[
'external_id' => $data['external_id'],
'name' => $data['name'],
'password' => Hash::make('password'),
]
);
$user->roles()->syncWithoutDetaching([$roles['dosen']->id, $roles['gpm']->id]);
Lecturer::firstOrCreate(
['user_id' => $user->id],
['employee_number' => $data['employee_number']]
);
}
// 4. MAHASISWA
$mahasiswaUsers = [
[
'external_id' => 'EXT-MHS-001',
'name' => 'Andi Pratama',
'email' => 'andi.pratama@student.kampus.ac.id',
'student_number' => '2021001001',
'cohort_year' => 2021,
],
[
'external_id' => 'EXT-MHS-002',
'name' => 'Bela Safitri',
'email' => 'bela.safitri@student.kampus.ac.id',
'student_number' => '2021001002',
'cohort_year' => 2021,
],
[
'external_id' => 'EXT-MHS-003',
'name' => 'Cahyo Nugroho',
'email' => 'cahyo.nugroho@student.kampus.ac.id',
'student_number' => '2021001003',
'cohort_year' => 2021,
],
[
'external_id' => 'EXT-MHS-004',
'name' => 'Dina Marlina',
'email' => 'dina.marlina@student.kampus.ac.id',
'student_number' => '2022001001',
'cohort_year' => 2022,
],
];
foreach ($mahasiswaUsers as $data) {
$user = User::firstOrCreate(
['email' => $data['email']],
[
'external_id' => $data['external_id'],
'name' => $data['name'],
'password' => Hash::make('password'),
]
);
$user->roles()->syncWithoutDetaching([$roles['mahasiswa']->id]);
Student::firstOrCreate(
['user_id' => $user->id],
[
'student_number' => $data['student_number'],
'cohort_year' => $data['cohort_year'],
]
);
}
}
}Setiap modul punya seeder sendiri untuk data spesifik modulnya. Seeder modul tidak dipanggil otomatis dari DatabaseSeeder β harus didaftarkan secara eksplisit atau dijalankan manual.
Contoh CapstoneSeeder.php:
<?php
namespace Modules\Capstone\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class CapstoneSeeder extends Seeder
{
public function run(): void
{
$this->call([
CapstonePeriodSeeder::class, // seed periode capstone
CapstoneTopicSeeder::class, // seed topik/judul dummy
]);
}
}Contoh BankSoalSeeder.php:
<?php
namespace Modules\BankSoal\Database\Seeders;
use Illuminate\Database\Seeder;
class BankSoalSeeder extends Seeder
{
public function run(): void
{
$this->call([
MataKuliahSeeder::class, // seed daftar mata kuliah
BankSoalDummySeeder::class, // seed soal dummy
]);
}
}php artisan db:seedMenjalankan
DatabaseSeederbeserta semua seeder yang didaftarkan di dalamnya.
# Hanya seed roles
php artisan db:seed --class=RoleSeeder
# Hanya seed user
php artisan db:seed --class=UserSeederKarena seeder modul berada di namespace berbeda, gunakan flag --class dengan fully qualified class name:
# Capstone
php artisan db:seed --class="Modules\Capstone\Database\Seeders\CapstoneSeeder"
# Bank Soal
php artisan db:seed --class="Modules\BankSoal\Database\Seeders\BankSoalSeeder"
# Kemahasiswaan
php artisan db:seed --class="Modules\Kemahasiswaan\Database\Seeders\KemahasiswaanSeeder"
# E-Office
php artisan db:seed --class="Modules\EOffice\Database\Seeders\EOfficeSeeder"# Hanya global seeder
php artisan migrate:fresh --seed
# Migrate fresh + seed semua modul sekaligus
php artisan migrate:fresh --seed && \
php artisan db:seed --class="Modules\Capstone\Database\Seeders\CapstoneSeeder" && \
php artisan db:seed --class="Modules\BankSoal\Database\Seeders\BankSoalSeeder" && \
php artisan db:seed --class="Modules\Kemahasiswaan\Database\Seeders\KemahasiswaanSeeder" && \
php artisan db:seed --class="Modules\EOffice\Database\Seeders\EOfficeSeeder"Jika ingin semua modul ikut ter-seed saat php artisan db:seed atau migrate:fresh --seed, tambahkan ke DatabaseSeeder:
// database/seeders/DatabaseSeeder.php
public function run(): void
{
$this->call([
// Global
RoleSeeder::class,
UserSeeder::class,
StudentSeeder::class,
LecturerSeeder::class,
// Modules β uncomment sesuai kebutuhan
\Modules\Capstone\Database\Seeders\CapstoneSeeder::class,
\Modules\BankSoal\Database\Seeders\BankSoalSeeder::class,
\Modules\Kemahasiswaan\Database\Seeders\KemahasiswaanSeeder::class,
\Modules\EOffice\Database\Seeders\EOfficeSeeder::class,
]);
}π‘ Tips: Di environment development, daftarkan semua modul seeder agar mudah reset & rebuild data. Di production, jalankan modul seeder secara manual sesuai kebutuhan untuk menghindari data dummy masuk ke production.
# Global seeder
php artisan make:seeder NamaSeeder
# Module seeder (buat manual di folder yang sesuai)
# Contoh: Modules/Capstone/Database/Seeders/CapstonePeriodSeeder.phpUntuk module seeder yang dibuat manual, gunakan namespace yang sesuai:
<?php
namespace Modules\Capstone\Database\Seeders;
use Illuminate\Database\Seeder;
class CapstonePeriodSeeder extends Seeder
{
public function run(): void
{
// ...
}
}
β οΈ Urutan penting! Seed global (roles,users) selalu duluan sebelum modul, karena modul seeder biasanya butuh foreign key ke tabel global.
β Gunakan
updateOrInsert()ataufirstOrCreate()agar seeder aman dijalankan berulang tanpa duplikasi data.
π Setelah seeding, clear cache Redis agar data lama tidak tersisa:
php artisan cache:clear
php artisan route:clear
php artisan config:clear
php artisan cache:clear
php artisan view:clear
# Clear semua sekaligus
php artisan optimize:clearphp artisan optimize
php artisan config:cache
php artisan route:cache
php artisan view:cache
composer dump-autoload -ophp artisan route:list
php artisan route:list | grep capstone
php artisan db:show
php artisan storage:link| Role | Code | Description |
|---|---|---|
| π΄ Superadmin | superadmin |
Full system access |
| π Admin | admin |
Administrative access |
| π‘ Dosen | dosen |
Lecturer/faculty access |
| π’ Mahasiswa | mahasiswa |
Student access (default) |
// Cek single role
$user->hasRole('dosen');
// Cek salah satu dari beberapa role
$user->hasAnyRole(['superadmin', 'admin']);
// Cek dengan filter module
$user->hasRole('dosen', 'capstone');
// Di route middleware
Route::middleware(['auth', 'role:superadmin|admin'])->group(function () {
// ...
});// Wajib dipanggil setiap kali roles user diubah
public function updateUserRoles(User $user, array $roleIds): void
{
$user->roles()->sync($roleIds);
$user->clearUserCache(); // hapus cache lama agar langsung efektif
}| Rule | Description |
|---|---|
| π« No Migration Edit | Jangan edit migration yang sudah dijalankan di production |
| π Use Prefix | Gunakan prefix sesuai module untuk semua tabel |
| π Clear Cache | Selalu clear cache setelah ubah route/config |
| π No .env Commit | Jangan commit file .env ke repository |
| β‘ Eager Loading | Gunakan with() untuk menghindari N+1 query problem |
| ποΈ Clear User Cache | Panggil clearUserCache() setiap kali update data/roles user |
| π Code Documentation | Tambahkan docblock untuk function public |
| π§ͺ Test Before Commit | Test fitur sebelum commit ke branch utama |
| π΄ Redis First | Pastikan Redis/Memurai jalan sebelum start Laravel |
| π± Safe Seeder | Gunakan updateOrInsert / firstOrCreate agar seeder idempotent |
git checkout -b feature/module-name-feature
git add .
git commit -m "feat(module): description"
git push origin feature/module-name-featurefeat(capstone): add topic submission feature
fix(bank-soal): resolve question duplication bug
docs(readme): update installation guide
refactor(kemahasiswaan): optimize event query
perf(auth): add redis caching for user roles
seed(capstone): add period and topic dummy data
Laravel Telescope adalah debug assistant bawaan Laravel yang memungkinkan kamu memantau setiap request yang masuk: berapa lama prosesnya, query apa saja yang dijalankan, apakah ada query lambat, exception apa yang terjadi, dan masih banyak lagi β semuanya lewat UI web yang rapi.
β οΈ Telescope hanya untuk environmentlocal/development. Jangan aktifkan di production karena menyimpan seluruh data request ke database dan berpotensi membocorkan informasi sensitif.
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migratePerintah telescope:install akan:
- Mempublish config ke
config/telescope.php - Mempublish assets (CSS/JS) ke
public/vendor/telescope - Mendaftarkan
TelescopeServiceProviderkebootstrap/providers.php
Pastikan Telescope tidak pernah load di production. Buka app/Providers/TelescopeServiceProvider.php dan verifikasi method register():
// app/Providers/TelescopeServiceProvider.php
public function register(): void
{
// Telescope hanya aktif di environment local
if ($this->app->isLocal()) {
$this->app->register(\Laravel\Telescope\TelescopeApplicationServiceProvider::class);
}
}Atau bisa juga via config/telescope.php:
// config/telescope.php
'enabled' => env('TELESCOPE_ENABLED', false),Dan di .env development:
TELESCOPE_ENABLED=trueβ Dengan cara ini, Telescope tidak akan aktif kecuali kamu eksplisit menyalakannya di
.envlokal.
Setelah server berjalan, buka:
http://localhost:8000/telescope
Ini adalah fitur utama yang paling berguna untuk profiling performa.
Buka Telescope β Requests. Kamu akan melihat tabel seperti ini:
| Method | Path | Status | Duration | Time |
|---|---|---|---|---|
| POST | /login | 302 | 387ms | 14:32:01 |
| GET | /dashboard | 200 | 210ms | 14:32:02 |
| GET | /superadmin/dashboard | 200 | 950ms | 14:32:05 |
Kolom Duration menunjukkan total waktu dari request masuk hingga response keluar.
Klik salah satu request untuk melihat breakdown lengkapnya:
Request Detail
βββ π Request Info
β βββ Method, URL, Status Code
β βββ Controller & Action yang dipanggil
β βββ Middleware yang dijalankan
β
βββ ποΈ Queries (paling penting untuk profiling!)
β βββ Jumlah query yang dieksekusi
β βββ Durasi tiap query (ms)
β βββ Raw SQL dengan binding-nya
β
βββ β‘ Cache
β βββ Cache hit / miss
β βββ Key yang diakses
β
βββ π¦ Session
β βββ Data session yang aktif
β
βββ π’ Response
βββ Status & response body (jika JSON)
Telescope bisa highlight otomatis request yang melebihi threshold tertentu. Konfigurasi di config/telescope.php:
// config/telescope.php
'slow_queries' => [
'enabled' => true,
'threshold' => 100, // query > 100ms dianggap lambat (dalam ms)
],
'slow_requests' => [
'enabled' => true,
'threshold' => 1000, // request > 1000ms dianggap lambat (dalam ms)
],Request dan query yang melewati threshold akan ditandai merah di dashboard Telescope.
Cek N+1 Query Problem:
Kalau kamu buka satu halaman dan di tab Queries muncul puluhan query dengan pola yang mirip-mirip, itu tanda N+1. Solusinya pakai eager loading with():
// β Tanpa eager loading β muncul N+1 di Telescope
$groups = CapstoneGroup::all();
foreach ($groups as $group) {
echo $group->students->count(); // query baru tiap iterasi
}
// β
Dengan eager loading β hanya 2 query di Telescope
$groups = CapstoneGroup::with('students')->get();Cek Cache Hit/Miss:
Di tab Cache, pastikan key seperti user:{id}:roles dan user:{id}:data berstatus hit (bukan miss) setelah login pertama. Kalau terus miss, berarti caching belum bekerja.
Bandingkan Before/After Optimasi:
Gunakan Telescope sebelum dan sesudah menerapkan perubahan (tambah index, eager loading, caching) untuk melihat penurunan duration secara konkret.
Data Telescope tersimpan di tabel telescope_entries dan telescope_entries_tags. Bersihkan secara berkala agar tidak memberatkan database lokal:
# Hapus semua data Telescope
php artisan telescope:clear
# Atau jalankan pruning otomatis (hapus data > 24 jam)
php artisan telescope:prune
# Jalankan pruning dengan custom hours
php artisan telescope:prune --hours=48Bisa juga dijadwalkan di routes/console.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('telescope:prune')->daily();Jika teammate tidak ingin install Telescope (misal di mesin yang resource-nya terbatas):
composer remove laravel/telescope
php artisan migrate:rollback # rollback migration telescopeHapus juga TelescopeServiceProvider dari bootstrap/providers.php jika masih terdaftar.
π‘ Karena diinstall dengan flag
--dev, Telescope tidak akan ikut ter-install di production saatcomposer install --no-dev.
β Route tidak berubah setelah edit
php artisan route:clear
php artisan config:clearβ Migration error "table already exists"
php artisan migrate:status
# Jika perlu reset (β οΈ data hilang)
php artisan migrate:freshβ Laravel terasa lambat / response > 2 detik
Pastikan checklist berikut:
- Redis/Memurai sudah jalan
.envsudah setCACHE_STORE=redisdanSESSION_DRIVER=redisconfig/auth.phpsudah pakaicached-eloquentdriver- Index database sudah dibuat di Supabase SQL Editor
config/database.phpsudah adaPDO::ATTR_PERSISTENT => true
php artisan config:clear
php artisan cache:clearβ Redis connection refused
Windows: Pastikan Memurai sudah jalan. Buka Start Menu β cari "Memurai" β Start. Atau cek di services.msc.
Linux/Mac:
sudo systemctl start redis-server # Linux
brew services start redis # MacVerifikasi:
memurai-cli ping # Windows
redis-cli ping # Linux/Mac
# Harus balik: PONGβ Unable to load php_redis.dll
Buka C:\xampp\php\php.ini, cari dan comment out:
;extension=php_redis.dllRestart XAMPP/Laragon, lalu jalankan ulang Laravel.
β Call to a member function contains() on array
Ini terjadi karena data dari cache berupa array biasa, bukan Collection. Pastikan selalu wrap dengan collect() sebelum memanggil method Collection:
// β Salah
$cached = Cache::get("user:{$id}:roles");
$cached->contains('superadmin');
// β
Benar
$userRoles = collect(Cache::get("user:{$id}:roles"))->pluck('name');
$userRoles->contains('superadmin');Jalankan php artisan cache:clear untuk hapus cache lama yang formatnya mungkin berbeda.
β The attribute [remember_token] does not exist
Pastikan remember_token tidak ada di $hidden di User.php, dan cacheUserData() menggunakan makeVisible(['remember_token']) sebelum serialize ke cache.
// User.php β jangan masukkan remember_token ke $hidden
protected $hidden = [
'password',
// remember_token TIDAK di-hidden
];β Authentication user provider [cached-eloquent] is not defined
Pastikan dua hal:
AppServiceProvider.phpsudah adaAuth::resolved(...)di methodregister()config/auth.phpsudah diupdate:
'providers' => [
'users' => [
'driver' => 'cached-eloquent',
'model' => App\Models\User::class,
],
],Lalu jalankan:
php artisan config:clearβ Supabase connection timeout
- Gunakan connection pooling port
6543bukan5432 - Pastikan region Supabase project di Singapore (
ap-southeast-1) bukan Mumbai - Test koneksi:
php artisan db:showβ Seeder error: Class not found (module seeder)
Pastikan namespace di file seeder modul sudah benar, lalu regenerate autoload:
composer dump-autoloadJalankan ulang seeder dengan fully qualified class name:
php artisan db:seed --class="Modules\Capstone\Database\Seeders\CapstoneSeeder"β Seeder error: Duplicate entry / unique constraint violation
Seeder dijalankan lebih dari sekali tanpa guard idempotent. Ganti insert() dengan updateOrInsert() atau firstOrCreate():
// β Akan error jika dijalankan dua kali
DB::table('roles')->insert(['name' => 'superadmin']);
// β
Aman dijalankan berulang
DB::table('roles')->updateOrInsert(
['name' => 'superadmin'],
['updated_at' => now()]
);Atau truncate tabel dulu sebelum insert (hati-hati di production):
DB::table('roles')->truncate();
DB::table('roles')->insert([...]);- π Microservices Migration - Isolasi per module
- ποΈ Database Per Module - Separate database untuk setiap modul
- π Enhanced RBAC - Permission-based access control
- π’ Multi-Tenant - Support multiple institutions
- π± Mobile App - Native mobile application
- π€ API Gateway - Centralized API management
- π Analytics Dashboard - System-wide reporting
- π Real-time Notifications - WebSocket integration
- Laravel Documentation
- Supabase Documentation
- PostgreSQL Documentation
- Predis Documentation
- Memurai (Redis for Windows)
Made with β€οΈ by Tim Capstone