E-learning platforms require robust course management and video delivery. Laravel provides the foundation for scalable educational applications. At ZIRA Software, we've built LMS platforms serving thousands of concurrent learners.
Database Schema
// Courses
Schema::create('courses', function (Blueprint $table) {
$table->id();
$table->foreignId('instructor_id')->constrained('users');
$table->string('title');
$table->string('slug')->unique();
$table->text('description');
$table->decimal('price', 10, 2)->default(0);
$table->string('thumbnail')->nullable();
$table->string('preview_video')->nullable();
$table->enum('status', ['draft', 'published', 'archived'])->default('draft');
$table->integer('duration_minutes')->default(0);
$table->timestamps();
});
// Sections/Modules
Schema::create('sections', function (Blueprint $table) {
$table->id();
$table->foreignId('course_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->integer('position')->default(0);
$table->timestamps();
});
// Lessons
Schema::create('lessons', function (Blueprint $table) {
$table->id();
$table->foreignId('section_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->string('slug');
$table->text('description')->nullable();
$table->enum('type', ['video', 'text', 'quiz', 'assignment']);
$table->string('video_url')->nullable();
$table->integer('duration_seconds')->default(0);
$table->text('content')->nullable();
$table->boolean('is_free')->default(false);
$table->integer('position')->default(0);
$table->timestamps();
});
// Enrollments
Schema::create('enrollments', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('course_id')->constrained()->onDelete('cascade');
$table->timestamp('enrolled_at');
$table->timestamp('completed_at')->nullable();
$table->timestamps();
$table->unique(['user_id', 'course_id']);
});
// Progress tracking
Schema::create('lesson_progress', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('lesson_id')->constrained()->onDelete('cascade');
$table->integer('watched_seconds')->default(0);
$table->boolean('completed')->default(false);
$table->timestamp('completed_at')->nullable();
$table->timestamps();
$table->unique(['user_id', 'lesson_id']);
});
Course Model
// app/Models/Course.php
class Course extends Model
{
public function instructor()
{
return $this->belongsTo(User::class, 'instructor_id');
}
public function sections()
{
return $this->hasMany(Section::class)->orderBy('position');
}
public function lessons()
{
return $this->hasManyThrough(Lesson::class, Section::class);
}
public function enrollments()
{
return $this->hasMany(Enrollment::class);
}
public function students()
{
return $this->belongsToMany(User::class, 'enrollments')
->withPivot('enrolled_at', 'completed_at')
->withTimestamps();
}
public function scopePublished($query)
{
return $query->where('status', 'published');
}
public function getProgressForUser(User $user): int
{
$totalLessons = $this->lessons()->count();
if ($totalLessons === 0) return 0;
$completedLessons = LessonProgress::where('user_id', $user->id)
->whereIn('lesson_id', $this->lessons()->pluck('id'))
->where('completed', true)
->count();
return (int) round(($completedLessons / $totalLessons) * 100);
}
}
Video Streaming with Signed URLs
// app/Services/VideoService.php
class VideoService
{
public function getStreamUrl(Lesson $lesson, User $user): string
{
// Check enrollment
if (!$lesson->is_free && !$user->isEnrolledIn($lesson->section->course)) {
throw new UnauthorizedException('Not enrolled in this course');
}
// Generate signed URL for S3/CloudFront
return URL::temporarySignedRoute(
'video.stream',
now()->addHours(4),
['lesson' => $lesson->id]
);
}
}
// app/Http/Controllers/VideoController.php
class VideoController extends Controller
{
public function stream(Request $request, Lesson $lesson)
{
if (!$request->hasValidSignature()) {
abort(401);
}
// Stream from S3
$client = Storage::disk('s3')->getClient();
$command = $client->getCommand('GetObject', [
'Bucket' => config('filesystems.disks.s3.bucket'),
'Key' => $lesson->video_path,
]);
$signedUrl = $client->createPresignedRequest($command, '+4 hours')
->getUri();
return redirect($signedUrl);
}
}
Progress Tracking
// app/Http/Controllers/ProgressController.php
class ProgressController extends Controller
{
public function update(Request $request, Lesson $lesson)
{
$validated = $request->validate([
'watched_seconds' => 'required|integer|min:0',
'completed' => 'boolean',
]);
$progress = LessonProgress::updateOrCreate(
[
'user_id' => auth()->id(),
'lesson_id' => $lesson->id,
],
[
'watched_seconds' => $validated['watched_seconds'],
'completed' => $validated['completed'] ?? false,
'completed_at' => $validated['completed'] ? now() : null,
]
);
// Check if course completed
$course = $lesson->section->course;
$this->checkCourseCompletion($course, auth()->user());
return response()->json($progress);
}
private function checkCourseCompletion(Course $course, User $user): void
{
$progress = $course->getProgressForUser($user);
if ($progress === 100) {
$enrollment = Enrollment::where('user_id', $user->id)
->where('course_id', $course->id)
->first();
if (!$enrollment->completed_at) {
$enrollment->update(['completed_at' => now()]);
event(new CourseCompleted($course, $user));
}
}
}
}
Certificate Generation
// app/Services/CertificateService.php
class CertificateService
{
public function generate(Course $course, User $user): Certificate
{
$enrollment = Enrollment::where('user_id', $user->id)
->where('course_id', $course->id)
->whereNotNull('completed_at')
->firstOrFail();
$certificate = Certificate::create([
'user_id' => $user->id,
'course_id' => $course->id,
'certificate_number' => $this->generateNumber(),
'issued_at' => now(),
]);
// Generate PDF
$pdf = Pdf::loadView('certificates.template', [
'certificate' => $certificate,
'user' => $user,
'course' => $course,
]);
$path = "certificates/{$certificate->certificate_number}.pdf";
Storage::put($path, $pdf->output());
$certificate->update(['pdf_path' => $path]);
return $certificate;
}
private function generateNumber(): string
{
return strtoupper(Str::random(4)) . '-' .
date('Y') . '-' .
str_pad(Certificate::count() + 1, 6, '0', STR_PAD_LEFT);
}
}
Video Player Frontend
// resources/js/components/VideoPlayer.vue
export default {
props: ['lesson', 'initialProgress'],
data() {
return {
player: null,
watchedSeconds: this.initialProgress?.watched_seconds || 0,
};
},
mounted() {
this.initPlayer();
},
methods: {
initPlayer() {
this.player = videojs(this.$refs.video, {
controls: true,
fluid: true,
});
// Resume from last position
this.player.currentTime(this.watchedSeconds);
// Track progress every 10 seconds
this.player.on('timeupdate', this.throttle(this.updateProgress, 10000));
// Mark complete when 90% watched
this.player.on('ended', () => this.markComplete());
},
async updateProgress() {
const currentTime = Math.floor(this.player.currentTime());
await fetch(`/api/lessons/${this.lesson.id}/progress`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
watched_seconds: currentTime,
completed: currentTime >= this.player.duration() * 0.9,
}),
});
},
},
};
Conclusion
E-learning platforms with Laravel combine course management, secure video streaming, and progress tracking. Proper architecture ensures scalability for growing student bases.
Building an e-learning platform? Contact ZIRA Software for LMS development.