Rebuild Laravel E-Learning

1

Part 1: Pengenalan & Mengapa Saya Upgrade

2

Part 2: Setup Laravel 12 + Filament

3

Part 3: Desain Skema Database

4

Part 4: Autentikasi & Role User

5

Part 5: Membangun Sistem Ujian

6

Part 6: Fitur Real-Time

7

Part 7: Strategi Testing

8

Part 8: Deployment

Artikel ini tersedia dalam Bahasa Inggris

šŸ‡¬šŸ‡§ Read in English

10 Maret 2026

šŸ‡®šŸ‡© Bahasa Indonesia

Laravel E-Learning Part 3: Desain Skema Database

Mendesain skema database yang robust untuk platform e-learning dengan users, mata pelajaran, ujian, soal, dan hasil menggunakan Laravel migrations.

4 min read


Gambaran Entity Relationship

Sebelum menulis kode, mari pahami model data kita:

Entity Relationship Diagram

E-Learning Database ERD

Gambaran Tabel

TabelDeskripsiRelasi Utama
usersSemua user (Admin, Guru, Siswa)Punya role via Spatie
subjectsMata pelajaran (Matematika, Fisika, dll.)Milik Guru
examsUjian/test individualMilik Subject, punya banyak Question
questionsSoal ujianMilik Exam
exam_attemptsSesi ujian siswaMilik Exam dan Student
answersJawaban siswaMilik ExamAttempt dan Question

Relasi Laravel Eloquent

ModelRelasiModel Terkait
User (Guru)hasManySubject
SubjectbelongsToUser
SubjecthasManyExam
ExambelongsToSubject
ExamhasManyQuestion
ExamhasManyExamAttempt
QuestionbelongsToExam
ExamAttemptbelongsToUser (Siswa)
ExamAttemptbelongsToExam
ExamAttempthasManyAnswer
AnswerbelongsToExamAttempt
AnswerbelongsToQuestion

Step 1: Buat Migrations

Tabel Subjects

php artisan make:migration create_subjects_table
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('subjects', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('code')->unique();
            $table->text('description')->nullable();
            $table->foreignId('teacher_id')->constrained('users')->onDelete('cascade');
            $table->boolean('is_active')->default(true);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('subjects');
    }
};

Tabel Exams

php artisan make:migration create_exams_table
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('exams', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description')->nullable();
            $table->foreignId('subject_id')->constrained()->onDelete('cascade');
            $table->integer('duration_minutes')->default(60);
            $table->integer('passing_score')->default(60);
            $table->integer('total_questions')->default(0);
            $table->dateTime('start_time');
            $table->dateTime('end_time');
            $table->enum('status', ['draft', 'published', 'closed'])->default('draft');
            $table->boolean('shuffle_questions')->default(false);
            $table->boolean('show_result')->default(true);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('exams');
    }
};

Tabel Questions

php artisan make:migration create_questions_table
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('questions', function (Blueprint $table) {
            $table->id();
            $table->foreignId('exam_id')->constrained()->onDelete('cascade');
            $table->text('question_text');
            $table->enum('question_type', ['multiple_choice', 'true_false', 'essay'])->default('multiple_choice');
            $table->json('options')->nullable(); // Untuk pilihan ganda
            $table->string('correct_answer');
            $table->integer('points')->default(1);
            $table->integer('order')->default(0);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('questions');
    }
};

Tabel Exam Attempts

php artisan make:migration create_exam_attempts_table
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('exam_attempts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('exam_id')->constrained()->onDelete('cascade');
            $table->foreignId('student_id')->constrained('users')->onDelete('cascade');
            $table->dateTime('started_at');
            $table->dateTime('completed_at')->nullable();
            $table->integer('score')->nullable();
            $table->integer('correct_answers')->default(0);
            $table->integer('total_answered')->default(0);
            $table->enum('status', ['in_progress', 'completed', 'timed_out'])->default('in_progress');
            $table->timestamps();
            
            $table->unique(['exam_id', 'student_id']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('exam_attempts');
    }
};

Tabel Answers

php artisan make:migration create_answers_table
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('answers', function (Blueprint $table) {
            $table->id();
            $table->foreignId('exam_attempt_id')->constrained()->onDelete('cascade');
            $table->foreignId('question_id')->constrained()->onDelete('cascade');
            $table->text('answer')->nullable();
            $table->boolean('is_correct')->nullable();
            $table->integer('points_earned')->default(0);
            $table->timestamps();
            
            $table->unique(['exam_attempt_id', 'question_id']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('answers');
    }
};

Step 2: Buat Models

Model Subject

php artisan make:model Subject
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Subject extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'code',
        'description',
        'teacher_id',
        'is_active',
    ];

    protected $casts = [
        'is_active' => 'boolean',
    ];

    public function teacher(): BelongsTo
    {
        return $this->belongsTo(User::class, 'teacher_id');
    }

    public function exams(): HasMany
    {
        return $this->hasMany(Exam::class);
    }
}

Model Exam

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Exam extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'description',
        'subject_id',
        'duration_minutes',
        'passing_score',
        'total_questions',
        'start_time',
        'end_time',
        'status',
        'shuffle_questions',
        'show_result',
    ];

    protected $casts = [
        'start_time' => 'datetime',
        'end_time' => 'datetime',
        'shuffle_questions' => 'boolean',
        'show_result' => 'boolean',
    ];

    public function subject(): BelongsTo
    {
        return $this->belongsTo(Subject::class);
    }

    public function questions(): HasMany
    {
        return $this->hasMany(Question::class);
    }

    public function attempts(): HasMany
    {
        return $this->hasMany(ExamAttempt::class);
    }

    public function isActive(): bool
    {
        $now = now();
        return $this->status === 'published' 
            && $now->between($this->start_time, $this->end_time);
    }
}

Model Question

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Question extends Model
{
    use HasFactory;

    protected $fillable = [
        'exam_id',
        'question_text',
        'question_type',
        'options',
        'correct_answer',
        'points',
        'order',
    ];

    protected $casts = [
        'options' => 'array',
    ];

    public function exam(): BelongsTo
    {
        return $this->belongsTo(Exam::class);
    }
}

Step 3: Jalankan Migrations

php artisan migrate

Step 4: Buat Seeders

php artisan make:seeder SubjectSeeder
<?php

namespace Database\Seeders;

use App\Models\Subject;
use App\Models\User;
use Illuminate\Database\Seeder;

class SubjectSeeder extends Seeder
{
    public function run(): void
    {
        $teacher = User::first();

        $subjects = [
            ['name' => 'Matematika', 'code' => 'MATH101'],
            ['name' => 'Fisika', 'code' => 'PHYS101'],
            ['name' => 'Bahasa Inggris', 'code' => 'ENG101'],
            ['name' => 'Ilmu Komputer', 'code' => 'CS101'],
        ];

        foreach ($subjects as $subject) {
            Subject::create([
                ...$subject,
                'teacher_id' => $teacher->id,
            ]);
        }
    }
}

Ringkasan

Kita sudah mendesain skema database lengkap dengan:

  • āœ… Mata pelajaran yang dikelola oleh guru
  • āœ… Ujian dengan pengaturan yang bisa dikonfigurasi
  • āœ… Soal dengan berbagai tipe
  • āœ… Percobaan ujian dan jawaban siswa
  • āœ… Relasi dan constraint yang tepat

Selanjutnya

Di Part 4, kita akan mengimplementasikan autentikasi dan role user:

  • Akses berbasis role (Admin, Guru, Siswa)
  • Panel Filament terpisah untuk role berbeda
  • Manajemen permission dengan Spatie

Lanjut ke Part 4: Autentikasi & Role User →

Lanjut Membaca

Previous article thumbnail

← Artikel Sebelumnya

Laravel E-Learning Part 2: Setting Up Laravel 12 with Filament Admin Panel

Artikel Selanjutnya →

Laravel E-Learning Part 4: Authentication & User Roles

Next article thumbnail