CREATE TABLE learning_path_courses (
id INT PRIMARY KEY AUTO_INCREMENT,
path_id INT NOT NULL,
course_id INT NOT NULL,
sort_order INT DEFAULT 0,
is_required BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (path_id) REFERENCES learning_paths(id) ON DELETE CASCADE,
FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
UNIQUE KEY unique_path_course (path_id, course_id)
);
-- User learning paths
CREATE TABLE user_learning_paths (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
path_id INT NOT NULL,
progress INT DEFAULT 0,
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (path_id) REFERENCES learning_paths(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_path (user_id, path_id)
);
-- Certificates
CREATE TABLE certificates (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
course_id INT NOT NULL,
certificate_number VARCHAR(100) UNIQUE NOT NULL,
issue_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
certificate_url VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_course_cert (user_id, course_id)
);
-- Instructor earnings
CREATE TABLE instructor_earnings (
id INT PRIMARY KEY AUTO_INCREMENT,
instructor_id INT NOT NULL,
enrollment_id INT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status ENUM('pending', 'paid', 'cancelled') DEFAULT 'pending',
paid_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (instructor_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (enrollment_id) REFERENCES enrollments(id) ON DELETE CASCADE,
INDEX idx_instructor (instructor_id)
);
-- Site settings
CREATE TABLE settings (
id INT PRIMARY KEY AUTO_INCREMENT,
setting_key VARCHAR(100) UNIQUE NOT NULL,
setting_value TEXT,
description TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Insert default settings
INSERT INTO settings (setting_key, setting_value, description) VALUES
('site_name', 'LearnHub', 'Website name'),
('site_description', 'Learn anything, anytime, anywhere', 'Site description'),
('currency', 'USD', 'Default currency'),
('currency_symbol', '$', 'Currency symbol'),
('platform_fee', '20.00', 'Platform fee percentage'),
('min_instructor_payout', '50.00', 'Minimum payout amount'),
('max_upload_size', '500', 'Maximum upload size in MB'),
('courses_per_page', '12', 'Number of courses per page'),
('recommendation_limit', '20', 'Number of recommendations to generate'),
('trending_days', '30', 'Days to consider for trending'),
('maintenance_mode', '0', 'Maintenance mode status'),
('registration_enabled', '1', 'Allow new registrations');
-- Insert sample categories
INSERT INTO categories (name, slug, description, icon) VALUES
('Development', 'development', 'Learn programming and web development', '💻'),
('Business', 'business', 'Entrepreneurship and business skills', '📊'),
('Finance', 'finance', 'Accounting and financial management', '💰'),
('IT & Software', 'it-software', 'IT certification and software training', '🖥️'),
('Design', 'design', 'Graphic design and creative skills', '🎨'),
('Marketing', 'marketing', 'Digital marketing and SEO', '📈'),
('Music', 'music', 'Learn instruments and music production', '🎵'),
('Photography', 'photography', 'Photography and video editing', '📸'),
('Health & Fitness', 'health-fitness', 'Wellness and personal training', '🏋️'),
('Language', 'language', 'Learn new languages', '🗣️');
BACKEND IMPLEMENTATION
config.php
<?php
session_start();
// Database configuration
define('DB_HOST', 'localhost');
define('DB_NAME', 'lms_platform');
define('DB_USER', 'root');
define('DB_PASS', '');
// Site configuration
define('SITE_URL', 'http://localhost/lms');
define('SITE_NAME', 'LearnHub');
define('UPLOAD_DIR', __DIR__ . '/uploads/');
define('MAX_FILE_SIZE', 500 * 1024 * 1024); // 500MB
define('TIMEZONE', 'UTC');
define('DEBUG_MODE', true);
// Currency settings
define('CURRENCY', 'USD');
define('CURRENCY_SYMBOL', '$');
define('PLATFORM_FEE', 20.00); // Platform fee percentage
define('MIN_PAYOUT', 50.00);
// Pagination
define('COURSES_PER_PAGE', 12);
define('RECOMMENDATION_LIMIT', 20);
// Set timezone
date_default_timezone_set(TIMEZONE);
// Error reporting
if (DEBUG_MODE) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
} else {
error_reporting(0);
ini_set('display_errors', 0);
}
// Database connection
try {
$pdo = new PDO(
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
DB_USER,
DB_PASS,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
} catch(PDOException $e) {
die("Connection failed: " . $e->getMessage());
}
// Create upload directories
$upload_dirs = [
UPLOAD_DIR,
UPLOAD_DIR . 'courses/',
UPLOAD_DIR . 'lessons/',
UPLOAD_DIR . 'assignments/',
UPLOAD_DIR . 'profiles/',
UPLOAD_DIR . 'certificates/'
];
foreach ($upload_dirs as $dir) {
if (!file_exists($dir)) {
mkdir($dir, 0777, true);
}
}
// Include helper functions
require_once 'helpers.php';
// Autoload classes
spl_autoload_register(function ($class) {
$file = __DIR__ . '/classes/' . $class . '.php';
if (file_exists($file)) {
require_once $file;
}
});
// Check if user is logged in
function isLoggedIn() {
return isset($_SESSION['user_id']);
}
// Get current user ID
function getCurrentUserId() {
return $_SESSION['user_id'] ?? null;
}
// Get current user type
function getUserType() {
return $_SESSION['user_type'] ?? null;
}
// Get current user data
function getCurrentUser() {
global $pdo;
if (!isLoggedIn()) return null;
static $user = null;
if ($user === null) {
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
}
return $user;
}
// Check if user is instructor
function isInstructor() {
return getUserType() === 'instructor';
}
// Check if user is admin
function isAdmin() {
return getUserType() === 'admin';
}
// Redirect function
function redirect($url) {
header("Location: " . SITE_URL . $url);
exit;
}
// JSON response function
function jsonResponse($data, $status = 200) {
http_response_code($status);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
?>
helpers.php
<?php
// Helper functions
// Sanitize input
function sanitize($input) {
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
// Generate random string
function generateRandomString($length = 32) {
return bin2hex(random_bytes($length / 2));
}
// Generate slug
function createSlug($string) {
$string = strtolower($string);
$string = preg_replace('/[^a-z0-9-]/', '-', $string);
$string = preg_replace('/-+/', '-', $string);
return trim($string, '-');
}
// Format price
function formatPrice($price) {
return CURRENCY_SYMBOL . number_format($price, 2);
}
// Format duration
function formatDuration($minutes) {
if ($minutes < 60) {
return $minutes . ' min';
}
$hours = floor($minutes / 60);
$mins = $minutes % 60;
return $hours . 'h ' . ($mins > 0 ? $mins . 'm' : '');
}
// Format date
function formatDate($date, $format = 'M j, Y') {
return date($format, strtotime($date));
}
// Time ago function
function timeAgo($datetime) {
$time = strtotime($datetime);
$now = time();
$diff = $now - $time;
if ($diff < 60) {
return $diff . ' seconds ago';
} elseif ($diff < 3600) {
$mins = floor($diff / 60);
return $mins . ' minute' . ($mins > 1 ? 's' : '') . ' ago';
} elseif ($diff < 86400) {
$hours = floor($diff / 3600);
return $hours . ' hour' . ($hours > 1 ? 's' : '') . ' ago';
} elseif ($diff < 2592000) {
$days = floor($diff / 86400);
return $days . ' day' . ($days > 1 ? 's' : '') . ' ago';
} elseif ($diff < 31536000) {
$months = floor($diff / 2592000);
return $months . ' month' . ($months > 1 ? 's' : '') . ' ago';
} else {
$years = floor($diff / 31536000);
return $years . ' year' . ($years > 1 ? 's' : '') . ' ago';
}
}
// Upload file
function uploadFile($file, $folder, $allowedTypes = ['jpg', 'jpeg', 'png', 'gif', 'mp4', 'pdf', 'doc', 'docx']) {
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'message' => 'Upload failed'];
}
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $allowedTypes)) {
return ['success' => false, 'message' => 'File type not allowed'];
}
if ($file['size'] > MAX_FILE_SIZE) {
return ['success' => false, 'message' => 'File too large'];
}
$filename = uniqid() . '_' . time() . '.' . $extension;
$filepath = UPLOAD_DIR . $folder . '/' . $filename;
if (move_uploaded_file($file['tmp_name'], $filepath)) {
return [
'success' => true,
'filename' => $filename,
'path' => 'uploads/' . $folder . '/' . $filename
];
}
return ['success' => false, 'message' => 'Failed to save file'];
}
// Get file URL
function getFileUrl($path) {
if (empty($path)) return null;
return SITE_URL . '/' . $path;
}
// Get course image URL
function getCourseImage($course) {
if (!empty($course['thumbnail_url'])) {
return getFileUrl($course['thumbnail_url']);
}
return SITE_URL . '/assets/images/course-placeholder.jpg';
}
// Get rating stars HTML
function getRatingStars($rating) {
$stars = '';
for ($i = 1; $i <= 5; $i++) {
if ($i <= $rating) {
$stars .= '<i class="fas fa-star text-yellow-400"></i>';
} elseif ($i - 0.5 <= $rating) {
$stars .= '<i class="fas fa-star-half-alt text-yellow-400"></i>';
} else {
$stars .= '<i class="far fa-star text-yellow-400"></i>';
}
}
return $stars;
}
// Calculate course progress
function calculateProgress($enrollmentId) {
global $pdo;
// Get total lessons in course
$stmt = $pdo->prepare("
SELECT COUNT(*) as total
FROM lessons l
JOIN course_sections cs ON l.section_id = cs.id
WHERE cs.course_id = (SELECT course_id FROM enrollments WHERE id = ?)
");
$stmt->execute([$enrollmentId]);
$total = $stmt->fetchColumn();
if ($total == 0) return 0;
// Get completed lessons
$stmt = $pdo->prepare("
SELECT COUNT(*) as completed
FROM lesson_progress
WHERE enrollment_id = ? AND is_completed = 1
");
$stmt->execute([$enrollmentId]);
$completed = $stmt->fetchColumn();
return round(($completed / $total) * 100);
}
// Generate certificate number
function generateCertificateNumber($userId, $courseId) {
return 'CERT-' . strtoupper(uniqid()) . '-' . $userId . '-' . $courseId;
}
// Calculate instructor earnings
function calculateEarnings($amount) {
$platformFee = ($amount * PLATFORM_FEE) / 100;
$instructorEarning = $amount - $platformFee;
return [
'total' => $amount,
'platform_fee' => $platformFee,
'instructor_earning' => $instructorEarning
];
}
// Log user interaction for recommendations
function logUserInteraction($userId, $courseId, $type, $metadata = []) {
global $pdo;
$stmt = $pdo->prepare("
INSERT INTO user_interactions (user_id, course_id, interaction_type, metadata)
VALUES (?, ?, ?, ?)
");
$stmt->execute([$userId, $courseId, $type, json_encode($metadata)]);
}
// Get user's skill level
function getSkillLevel($proficiency) {
$levels = [
'beginner' => 1,
'intermediate' => 2,
'advanced' => 3,
'expert' => 4
];
return $levels[$proficiency] ?? 1;
}
// Check if user has prerequisite skills
function checkPrerequisites($userId, $courseId) {
global $pdo;
// Get course prerequisites
$stmt = $pdo->prepare("
SELECT prerequisite_course_id FROM course_prerequisites WHERE course_id = ?
");
$stmt->execute([$courseId]);
$prerequisites = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (empty($prerequisites)) {
return ['has_prerequisites' => true];
}
// Check if user completed prerequisites
$stmt = $pdo->prepare("
SELECT COUNT(*) FROM enrollments
WHERE user_id = ? AND course_id IN (" . implode(',', $prerequisites) . ") AND is_completed = 1
");
$stmt->execute([$userId]);
$completed = $stmt->fetchColumn();
$missing = array_diff($prerequisites, $stmt->fetchAll(PDO::FETCH_COLUMN));
return [
'has_prerequisites' => $completed == count($prerequisites),
'completed' => $completed,
'total' => count($prerequisites),
'missing' => $missing
];
}
// Get course difficulty level
function getDifficultyLevel($level) {
$levels = [
'beginner' => 1,
'intermediate' => 2,
'advanced' => 3,
'all-levels' => 2
];
return $levels[$level] ?? 1;
}
classes/CourseRecommender.php (Core Recommendation Algorithm)
<?php
class CourseRecommender {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
// Main recommendation engine
public function getRecommendations($userId, $type = 'personalized', $limit = RECOMMENDATION_LIMIT) {
// Check cache first
$cached = $this->getCachedRecommendations($userId, $type, $limit);
if ($cached) {
return $cached;
}
switch ($type) {
case 'personalized':
$recommendations = $this->getPersonalizedRecommendations($userId, $limit);
break;
case 'trending':
$recommendations = $this->getTrendingCourses($limit);
break;
case 'popular':
$recommendations = $this->getPopularCourses($limit);
break;
case 'new':
$recommendations = $this->getNewCourses($limit);
break;
case 'skill_gap':
$recommendations = $this->getSkillGapRecommendations($userId, $limit);
break;
case 'similar':
$courseId = $_GET['course_id'] ?? 0;
$recommendations = $this->getSimilarCourses($courseId, $limit);
break;
case 'learning_path':
$recommendations = $this->getLearningPathRecommendations($userId, $limit);
break;
case 'category':
$categoryId = $_GET['category_id'] ?? 0;
$recommendations = $this->getCategoryRecommendations($userId, $categoryId, $limit);
break;
default:
$recommendations = [];
}
// Cache recommendations
$this->cacheRecommendations($userId, $type, $recommendations);
return $recommendations;
}
// Personalized recommendations based on user profile and behavior
private function getPersonalizedRecommendations($userId, $limit) {
// Get user profile
$userProfile = $this->buildUserProfile($userId);
if (empty($userProfile['interests']) && empty($userProfile['skills'])) {
// Cold start: recommend popular courses
return $this->getPopularCourses($limit);
}
// Get candidate courses (exclude enrolled and completed)
$candidates = $this->getCandidateCourses($userId);
// Score each candidate
$recommendations = [];
foreach ($candidates as $course) {
$score = $this->calculateCourseScore($course, $userProfile);
if ($score > 0.3) {
$recommendations[] = [
'course' => $course,
'score' => $score,
'reasons' => $this->getRecommendationReasons($course, $userProfile)
];
}
}
// Sort by score
usort($recommendations, function($a, $b) {
return $b['score'] <=> $a['score'];
});
return array_slice($recommendations, 0, $limit);
}
// Build user profile from interactions, skills, and goals
private function buildUserProfile($userId) {
$profile = [
'interests' => [],
'skills' => [],
'categories' => [],
'levels' => [],
'completed_courses' => [],
'avg_rating' => 0,
'total_interactions' => 0
];
// Get user skills
$stmt = $this->pdo->prepare("
SELECT skill_name, proficiency_level FROM user_skills WHERE user_id = ?
");
$stmt->execute([$userId]);
$skills = $stmt->fetchAll();
foreach ($skills as $skill) {
$profile['skills'][$skill['skill_name']] = getSkillLevel($skill['proficiency_level']);
}
// Get learning goals
$stmt = $this->pdo->prepare("
SELECT goal_title FROM learning_goals WHERE user_id = ? AND is_achieved = 0
");
$stmt->execute([$userId]);
$goals = $stmt->fetchAll();
foreach ($goals as $goal) {
$profile['interests'] = array_merge($profile['interests'], explode(' ', $goal['goal_title']));
}
// Get user interactions
$stmt = $this->pdo->prepare("
SELECT ui.*, c.category_id, c.level
FROM user_interactions ui
JOIN courses c ON ui.course_id = c.id
WHERE ui.user_id = ?
ORDER BY ui.created_at DESC
LIMIT 100
");
$stmt->execute([$userId]);
$interactions = $stmt->fetchAll();
$profile['total_interactions'] = count($interactions);
$totalRating = 0;
$ratingCount = 0;
foreach ($interactions as $interaction) {
// Track category preferences
if ($interaction['category_id']) {
$profile['categories'][$interaction['category_id']] =
($profile['categories'][$interaction['category_id']] ?? 0) + $this->getInteractionWeight($interaction['interaction_type']);
}
// Track level preferences
if ($interaction['level']) {
$profile['levels'][$interaction['level']] =
($profile['levels'][$interaction['level']] ?? 0) + $this->getInteractionWeight($interaction['interaction_type']);
}
// Track completed courses
if ($interaction['interaction_type'] == 'complete') {
$profile['completed_courses'][] = $interaction['course_id'];
}
// Get ratings from reviews
if ($interaction['interaction_type'] == 'review') {
$metadata = json_decode($interaction['metadata'], true);
if (isset($metadata['rating'])) {
$totalRating += $metadata['rating'];
$ratingCount++;
}
}
}
// Calculate average rating given
$profile['avg_rating'] = $ratingCount > 0 ? $totalRating / $ratingCount : 3.5;
// Extract interests from course titles/descriptions
foreach ($interactions as $interaction) {
if ($interaction['interaction_type'] == 'view' || $interaction['interaction_type'] == 'enroll') {
$course = $this->getCourseDetails($interaction['course_id']);
if ($course) {
$words = explode(' ', $course['title'] . ' ' . ($course['subtitle'] ?? ''));
$profile['interests'] = array_merge($profile['interests'], array_slice($words, 0, 5));
}
}
}
// Normalize preferences
foreach ($profile['categories'] as &$count) {
$count = $count / $profile['total_interactions'];
}
foreach ($profile['levels'] as &$count) {
$count = $count / $profile['total_interactions'];
}
return $profile;
}
// Get weight for interaction type
private function getInteractionWeight($type) {
$weights = [
'complete' => 1.0,
'enroll' => 0.8,
'review' => 0.7,
'wishlist' => 0.6,
'view' => 0.3,
'search' => 0.2
];
return $weights[$type] ?? 0.1;
}
// Get course details
private function getCourseDetails($courseId) {
$stmt = $this->pdo->prepare("
SELECT * FROM courses WHERE id = ?
");
$stmt->execute([$courseId]);
return $stmt->fetch();
}
// Calculate course relevance score
private function calculateCourseScore($course, $userProfile) {
$weights = [
'category' => 0.25,
'level' => 0.15,
'skills' => 0.30,
'popularity' => 0.15,
'recency' => 0.10,
'prerequisites' => 0.05
];
$scores = [
'category' => $this->calculateCategoryScore($course, $userProfile),
'level' => $this->calculateLevelScore($course, $userProfile),
'skills' => $this->calculateSkillScore($course, $userProfile),
'popularity' => $this->calculatePopularityScore($course),
'recency' => $this->calculateRecencyScore($course),
'prerequisites' => $this->calculatePrerequisiteScore($course, $userProfile)
];
$total = 0;
foreach ($weights as $factor => $weight) {
$total += $scores[$factor] * $weight;
}
return $total;
}
// Calculate category match score
private function calculateCategoryScore($course, $userProfile) {
if (!$course['category_id'] || empty($userProfile['categories'])) {
return 0.5;
}
return $userProfile['categories'][$course['category_id']] ?? 0.3;
}
// Calculate level match score
private function calculateLevelScore($course, $userProfile) {
if (empty($userProfile['levels'])) {
return 0.5;
}
$courseLevel = getDifficultyLevel($course['level']);
$preferredLevel = $this->getPreferredLevel($userProfile['levels']);
// Calculate how close the levels are
$levelDiff = abs($courseLevel - $preferredLevel);
return max(0, 1 - ($levelDiff / 3));
}
// Get preferred difficulty level
private function getPreferredLevel($levels) {
$weightedSum = 0;
$totalWeight = 0;
foreach ($levels as $level => $weight) {
$levelValue = getDifficultyLevel($level);
$weightedSum += $levelValue * $weight;
$totalWeight += $weight;
}
return $totalWeight > 0 ? $weightedSum / $totalWeight : 2;
}
// Calculate skill match score
private function calculateSkillScore($course, $userProfile) {
// Get course tags/skills
$stmt = $this->pdo->prepare("
SELECT tag FROM course_tags WHERE course_id = ?
");
$stmt->execute([$course['id']]);
$courseSkills = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (empty($courseSkills) || empty($userProfile['skills'])) {
return 0.5;
}
$score = 0;
$matchedSkills = 0;
foreach ($courseSkills as $skill) {
if (isset($userProfile['skills'][$skill])) {
$score += $userProfile['skills'][$skill];
$matchedSkills++;
}
}
if ($matchedSkills > 0) {
return $score / (count($courseSkills) * 4); // Normalize to 0-1
}
return 0.2;
}
// Calculate popularity score
private function calculatePopularityScore($course) {
$students = min($course['students_count'] ?? 0, 10000);
$rating = $course['rating_avg'] ?? 0;
$studentScore = $students / 10000;
$ratingScore = $rating / 5;
return ($studentScore * 0.6 + $ratingScore * 0.4);
}
// Calculate recency score
private function calculateRecencyScore($course) {
$created = strtotime($course['created_at']);
$now = time();
$daysOld = ($now - $created) / (60 * 60 * 24);
// Newer courses get higher scores (decay over 90 days)
return max(0, 1 - ($daysOld / 90));
}
// Calculate prerequisite score
private function calculatePrerequisiteScore($course, $userProfile) {
// Check if user has completed prerequisites
$completedCourses = $userProfile['completed_courses'] ?? [];
$stmt = $this->pdo->prepare("
SELECT prerequisite_course_id FROM course_prerequisites WHERE course_id = ?
");
$stmt->execute([$course['id']]);
$prerequisites = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (empty($prerequisites)) {
return 1.0;
}
$completed = 0;
foreach ($prerequisites as $pre) {
if (in_array($pre, $completedCourses)) {
$completed++;
}
}
return $completed / count($prerequisites);
}
// Get candidate courses (exclude enrolled and completed)
private function getCandidateCourses($userId) {
$stmt = $this->pdo->prepare("
SELECT c.*, cat.name as category_name, u.full_name as instructor_name
FROM courses c
LEFT JOIN categories cat ON c.category_id = cat.id
LEFT JOIN users u ON c.instructor_id = u.id
WHERE c.is_published = 1 AND c.is_approved = 1
AND c.id NOT IN (
SELECT course_id FROM enrollments WHERE user_id = ?
UNION
SELECT course_id FROM wishlists WHERE user_id = ?
)
ORDER BY c.created_at DESC
LIMIT 200
");
$stmt->execute([$userId, $userId]);
return $stmt->fetchAll();
}
// Get trending courses (based on recent enrollments)
public function getTrendingCourses($limit) {
$stmt = $this->pdo->prepare("
SELECT c.*,
COUNT(DISTINCT e.id) as recent_enrollments,
cat.name as category_name,
u.full_name as instructor_name
FROM courses c
LEFT JOIN enrollments e ON c.id = e.course_id
AND e.enrolled_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
LEFT JOIN categories cat ON c.category_id = cat.id
LEFT JOIN users u ON c.instructor_id = u.id
WHERE c.is_published = 1 AND c.is_approved = 1
GROUP BY c.id
ORDER BY recent_enrollments DESC, c.rating_avg DESC
LIMIT ?
");
$stmt->execute([$limit]);
return $stmt->fetchAll();
}
// Get popular courses (all-time bestsellers)
public function getPopularCourses($limit) {
$stmt = $this->pdo->prepare("
SELECT c.*,
cat.name as category_name,
u.full_name as instructor_name
FROM courses c
LEFT JOIN categories cat ON c.category_id = cat.id
LEFT JOIN users u ON c.instructor_id = u.id
WHERE c.is_published = 1 AND c.is_approved = 1
ORDER BY c.students_count DESC, c.rating_avg DESC
LIMIT ?
");
$stmt->execute([$limit]);
return $stmt->fetchAll();
}
// Get new courses
public function getNewCourses($limit) {
$stmt = $this->pdo->prepare("
SELECT c.*,
cat.name as category_name,
u.full_name as instructor_name
FROM courses c
LEFT JOIN categories cat ON c.category_id = cat.id
LEFT JOIN users u ON c.instructor_id = u.id
WHERE c.is_published = 1 AND c.is_approved = 1
ORDER BY c.created_at DESC
LIMIT ?
");
$stmt->execute([$limit]);
return $stmt->fetchAll();
}
// Get skill gap recommendations
public function getSkillGapRecommendations($userId, $limit) {
// Get user's skill gaps
$stmt = $this->pdo->prepare("
SELECT * FROM skill_gaps WHERE user_id = ? ORDER BY priority DESC
");
$stmt->execute([$userId]);
$gaps = $stmt->fetchAll();
if (empty($gaps)) {
return $this->getPersonalizedRecommendations($userId, $limit);
}
$recommendations = [];
foreach ($gaps as $gap) {
// Find courses that teach this skill at the right level
$stmt = $this->pdo->prepare("
SELECT c.*, cat.name as category_name, u.full_name as instructor_name
FROM courses c
JOIN course_tags ct ON c.id = ct.course_id
LEFT JOIN categories cat ON c.category_id = cat.id
LEFT JOIN users u ON c.instructor_id = u.id
WHERE ct.tag = ?
AND c.level <= ?
AND c.is_published = 1 AND c.is_approved = 1
AND c.id NOT IN (SELECT course_id FROM enrollments WHERE user_id = ?)
ORDER BY c.rating_avg DESC, c.students_count DESC
LIMIT 3
");
$stmt->execute([$gap['skill_name'], $gap['desired_level'], $userId]);
$courses = $stmt->fetchAll();
foreach ($courses as $course) {
$recommendations[] = [
'course' => $course,
'score' => 0.9,
'reasons' => ['Fills your skill gap in ' . $gap['skill_name']]
];
}
}
return array_slice($recommendations, 0, $limit);
}
// Get similar courses
public function getSimilarCourses($courseId, $limit) {
// Check pre-computed similarity
$stmt = $this->pdo->prepare("
SELECT c.*, cs.similarity_score,
cat.name as category_name,
u.full_name as instructor_name
FROM course_similarity cs
JOIN courses c ON cs.course_id_2 = c.id
LEFT JOIN categories cat ON c.category_id = cat.id
LEFT JOIN users u ON c.instructor_id = u.id
WHERE cs.course_id_1 = ?
ORDER BY cs.similarity_score DESC
LIMIT ?
");
$stmt->execute([$courseId, $limit]);
$similar = $stmt->fetchAll();
if (!empty($similar)) {
return $similar;
}
// Calculate on the fly based on tags and category
$stmt = $this->pdo->prepare("
SELECT c.*,
COUNT(DISTINCT ct1.tag) as match_count,
cat.name as category_name,
u.full_name as instructor_name
FROM courses c
JOIN course_tags ct1 ON c.id = ct1.course_id
JOIN course_tags ct2 ON ct1.tag = ct2.tag
LEFT JOIN categories cat ON c.category_id = cat.id
LEFT JOIN users u ON c.instructor_id = u.id
WHERE ct2.course_id = ?
AND c.id != ?
AND c.is_published = 1 AND c.is_approved = 1
GROUP BY c.id
ORDER BY match_count DESC, c.rating_avg DESC
LIMIT ?
");
$stmt->execute([$courseId, $courseId, $limit]);
return $stmt->fetchAll();
}
// Get learning path recommendations
public function getLearningPathRecommendations($userId, $limit) {
// Get user's current paths
$stmt = $this->pdo->prepare("
SELECT lp.*, ulp.progress
FROM learning_paths lp
JOIN user_learning_paths ulp ON lp.id = ulp.path_id
WHERE ulp.user_id = ? AND ulp.completed_at IS NULL
ORDER BY ulp.progress DESC
");
$stmt->execute([$userId]);
$currentPaths = $stmt->fetchAll();
if (empty($currentPaths)) {
// Recommend popular learning paths
$stmt = $this->pdo->prepare("
SELECT lp.*, COUNT(ulp.id) as students
FROM learning_paths lp
LEFT JOIN user_learning_paths ulp ON lp.id = ulp.path_id
WHERE lp.is_active = 1
GROUP BY lp.id
ORDER BY students DESC, lp.created_at DESC
LIMIT ?
");
$stmt->execute([$limit]);
return $stmt->fetchAll();
}
// Get next courses in current paths
$recommendations = [];
foreach ($currentPaths as $path) {
$stmt = $this->pdo->prepare("
SELECT c.*, lpc.sort_order,
cat.name as category_name,
u.full_name as instructor_name
FROM learning_path_courses lpc
JOIN courses c ON lpc.course_id = c.id
LEFT JOIN categories cat ON c.category_id = cat.id
LEFT JOIN users u ON c.instructor_id = u.id
WHERE lpc.path_id = ? AND lpc.sort_order > ?
AND c.id NOT IN (SELECT course_id FROM enrollments WHERE user_id = ?)
ORDER BY lpc.sort_order ASC
LIMIT 2
");
$stmt->execute([$path['id'], $path['progress'], $userId]);
$nextCourses = $stmt->fetchAll();
foreach ($nextCourses as $course) {
$recommendations[] = [
'course' => $course,
'score' => 0.95,
'reasons' => ['Next in your learning path: ' . $path['title']]
];
}
}
return array_slice($recommendations, 0, $limit);
}
// Get category-based recommendations
public function getCategoryRecommendations($userId, $categoryId, $limit) {
// Get user's preferred courses in this category
$stmt = $this->pdo->prepare("
SELECT c.*, ui.interaction_type
FROM user_interactions ui
JOIN courses c ON ui.course_id = c.id
WHERE ui.user_id = ? AND c.category_id = ?
ORDER BY ui.created_at DESC
LIMIT 10
");
$stmt->execute([$userId, $categoryId]);
$preferred = $stmt->fetchAll();
if (empty($preferred)) {
// Return popular in category
$stmt = $this->pdo->prepare("
SELECT c.*, cat.name as category_name, u.full_name as instructor_name
FROM courses c
LEFT JOIN categories cat ON c.category_id = cat.id
LEFT JOIN users u ON c.instructor_id = u.id
WHERE c.category_id = ? AND c.is_published = 1 AND c.is_approved = 1
ORDER BY c.students_count DESC, c.rating_avg DESC
LIMIT ?
");
$stmt->execute([$categoryId, $limit]);
return $stmt->fetchAll();
}
// Build profile from preferred courses and recommend similar
$profile = $this->buildProfileFromCourses($preferred);
return $this->getCoursesByProfile($profile, $categoryId, $limit);
}
// Build profile from a list of courses
private function buildProfileFromCourses($courses) {
$profile = [
'tags' => [],
'levels' => [],
'instructors' => []
];
foreach ($courses as $course) {
// Get course tags
$stmt = $this->pdo->prepare("SELECT tag FROM course_tags WHERE course_id = ?");
$stmt->execute([$course['id']]);
$tags = $stmt->fetchAll(PDO::FETCH_COLUMN);
foreach ($tags as $tag) {
$profile['tags'][$tag] = ($profile['tags'][$tag] ?? 0) + 1;
}
// Track levels
$profile['levels'][$course['level']] = ($profile['levels'][$course['level']] ?? 0) + 1;
// Track instructors
$profile['instructors'][$course['instructor_id']] = ($profile['instructors'][$course['instructor_id']] ?? 0) + 1;
}
// Normalize
$total = count($courses);
foreach ($profile as &$values) {
if (is_array($values)) {
foreach ($values as &$value) {
$value = $value / $total;
}
}
}
return $profile;
}
// Get courses matching profile
private function getCoursesByProfile($profile, $categoryId, $limit) {
// This is a simplified version - in production, you'd use more sophisticated matching
$tags = array_keys($profile['tags']);
$tagPlaceholders = implode(',', array_fill(0, count($tags), '?'));
$stmt = $this->pdo->prepare("
SELECT c.*,
COUNT(DISTINCT ct.tag) as tag_matches,
cat.name as category_name,
u.full_name as instructor_name
FROM courses c
JOIN course_tags ct ON c.id = ct.course_id
LEFT JOIN categories cat ON c.category_id = cat.id
LEFT JOIN users u ON c.instructor_id = u.id
WHERE c.category_id = ?
AND ct.tag IN ($tagPlaceholders)
AND c.is_published = 1 AND c.is_approved = 1
GROUP BY c.id
ORDER BY tag_matches DESC, c.rating_avg DESC
LIMIT ?
");
$params = array_merge([$categoryId], $tags, [$limit]);
$stmt->execute($params);
return $stmt->fetchAll();
}
// Get recommendation reasons
private function getRecommendationReasons($course, $userProfile) {
$reasons = [];
if (!empty($userProfile['categories']) && isset($userProfile['categories'][$course['category_id']])) {
$reasons[] = 'Based on your interest in this category';
}
if ($this->calculateSkillScore($course, $userProfile) > 0.5) {
$reasons[] = 'Matches your skills and interests';
}
if ($course['students_count'] > 1000) {
$reasons[] = 'Popular among students';
}
if ($course['rating_avg'] > 4.5) {
$reasons[] = 'Highly rated by students';
}
if (empty($reasons)) {
$reasons[] = 'Recommended for you';
}
return $reasons;
}
// Cache recommendations
private function cacheRecommendations($userId, $type, $recommendations) {
// Clear old cache
$stmt = $this->pdo->prepare("
DELETE FROM course_recommendations
WHERE user_id = ? AND reason = ?
");
$stmt->execute([$userId, $type]);
// Insert new recommendations
$stmt = $this->pdo->prepare("
INSERT INTO course_recommendations (user_id, course_id, score, reason, expires_at)
VALUES (?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL 1 DAY))
");
foreach ($recommendations as $rec) {
$courseId = is_array($rec) ? ($rec['course']['id'] ?? $rec['id']) : $rec['id'];
$score = is_array($rec) ? ($rec['score'] ?? 0.5) : 0.5;
$stmt->execute([
$userId,
$courseId,
$score,
$type
]);
}
}
// Get cached recommendations
private function getCachedRecommendations($userId, $type, $limit) {
$stmt = $this->pdo->prepare("
SELECT cr.*, c.*, cat.name as category_name, u.full_name as instructor_name
FROM course_recommendations cr
JOIN courses c ON cr.course_id = c.id
LEFT JOIN categories cat ON c.category_id = cat.id
LEFT JOIN users u ON c.instructor_id = u.id
WHERE cr.user_id = ? AND cr.reason = ? AND cr.expires_at > NOW()
ORDER BY cr.score DESC
LIMIT ?
");
$stmt->execute([$userId, $type, $limit]);
return $stmt->fetchAll();
}
// Pre-compute course similarity (run via cron)
public function computeCourseSimilarity() {
// Clear existing
$this->pdo->exec("TRUNCATE TABLE course_similarity");
// Get all published courses
$courses = $this->pdo->query("
SELECT id, category_id, title, description
FROM courses
WHERE is_published = 1 AND is_approved = 1
")->fetchAll();
$insertStmt = $this->pdo->prepare("
INSERT INTO course_similarity (course_id_1, course_id_2, similarity_score, based_on)
VALUES (?, ?, ?, ?)
");
foreach ($courses as $c1) {
foreach ($courses as $c2) {
if ($c1['id'] >= $c2['id']) continue;
$score = 0;
$basedOn = [];
// Category similarity
if ($c1['category_id'] == $c2['category_id']) {
$score += 0.4;
$basedOn[] = 'category';
}
// Tag similarity
$tagScore = $this->getTagSimilarity($c1['id'], $c2['id']);
$score += $tagScore * 0.3;
if ($tagScore > 0.3) $basedOn[] = 'tags';
// Title/description similarity (simple word overlap)
$textScore = $this->getTextSimilarity($c1['title'] . ' ' . $c1['description'],
$c2['title'] . ' ' . $c2['description']);
$score += $textScore * 0.3;
if ($textScore > 0.2) $basedOn[] = 'content';
if ($score > 0.3) {
$insertStmt->execute([
$c1['id'],
$c2['id'],
$score,
implode(',', $basedOn)
]);
}
}
}
}
// Get tag similarity between two courses
private function getTagSimilarity($courseId1, $courseId2) {
$stmt = $this->pdo->prepare("
SELECT COUNT(*) as common_tags
FROM course_tags ct1
JOIN course_tags ct2 ON ct1.tag = ct2.tag
WHERE ct1.course_id = ? AND ct2.course_id = ?
");
$stmt->execute([$courseId1, $courseId2]);
$common = $stmt->fetchColumn();
$stmt = $this->pdo->prepare("
SELECT COUNT(*) as total FROM course_tags WHERE course_id = ?
");
$stmt->execute([$courseId1]);
$total1 = $stmt->fetchColumn();
$stmt->execute([$courseId2]);
$total2 = $stmt->fetchColumn();
$total = max($total1, $total2);
return $total > 0 ? $common / $total : 0;
}
// Get text similarity using simple word overlap
private function getTextSimilarity($text1, $text2) {
$words1 = array_unique(str_word_count(strtolower($text1), 1));
$words2 = array_unique(str_word_count(strtolower($text2), 1));
$common = array_intersect($words1, $words2);
$total = max(count($words1), count($words2));
return $total > 0 ? count($common) / $total : 0;
}
}
classes/Course.php
<?php
class Course {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
// Get course by ID
public function getById($id) {
$stmt = $this->pdo->prepare("
SELECT c.*,
cat.name as category_name,
cat.slug as category_slug,
u.full_name as instructor_name,
u.profile_pic as instructor_pic,
u.bio as instructor_bio,
(SELECT COUNT(*) FROM enrollments WHERE course_id = c.id) as total_students,
(SELECT COUNT(*) FROM course_reviews WHERE course_id = c.id) as total_reviews,
(SELECT AVG(rating) FROM course_reviews WHERE course_id = c.id) as avg_rating
FROM courses c
LEFT JOIN categories cat ON c.category_id = cat.id
LEFT JOIN users u ON c.instructor_id = u.id
WHERE c.id = ?
");
$stmt->execute([$id]);
$course = $stmt->fetch();
if ($course) {
// Get sections and lessons
$course['sections'] = $this->getSections($id);
// Get tags
$course['tags'] = $this->getTags($id);
// Get prerequisites
$course['prerequisites'] = $this->getPrerequisites($id);
// Get reviews
$course['reviews'] = $this->getReviews($id, 5);
}
return $course;
}
// Get course by slug
public function getBySlug($slug) {
$stmt = $this->pdo->prepare("SELECT id FROM courses WHERE slug = ?");
$stmt->execute([$slug]);
$course = $stmt->fetch();
if ($course) {
return $this->getById($course['id']);
}
return null;
}
// Get course sections with lessons
public function getSections($courseId) {
$stmt = $this->pdo->prepare("
SELECT * FROM course_sections
WHERE course_id = ?
ORDER BY sort_order ASC
");
$stmt->execute([$courseId]);
$sections = $stmt->fetchAll();
foreach ($sections as &$section) {
$section['lessons'] = $this->getLessons($section['id']);
}
return $sections;
}
// Get lessons in a section
public function getLessons($sectionId) {
$stmt = $this->pdo->prepare("
SELECT * FROM lessons
WHERE section_id = ?
ORDER BY sort_order ASC
");
$stmt->execute([$sectionId]);
return $stmt->fetchAll();
}
// Get course tags
public function getTags($courseId) {
$stmt = $this->pdo->prepare("
SELECT tag FROM course_tags WHERE course_id = ?
");
$stmt->execute([$courseId]);
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
// Get course prerequisites
public function getPrerequisites($courseId) {
$stmt = $this->pdo->prepare("
SELECT c.* FROM course_prerequisites cp
JOIN courses c ON cp.prerequisite_course_id = c.id
WHERE cp.course_id = ?
");
$stmt->execute([$courseId]);
return $stmt->fetchAll();
}
// Get course reviews
public function getReviews($courseId, $limit = 10, $offset = 0) {
$stmt = $this->pdo->prepare("
SELECT r.*, u.full_name, u.profile_pic
FROM course_reviews r
JOIN users u ON r.user_id = u.id
WHERE r.course_id = ? AND r.is_approved = 1
ORDER BY r.created_at DESC
LIMIT ? OFFSET ?
");
$stmt->execute([$courseId, $limit, $offset]);
return $stmt->fetchAll();
}
// Search courses
public function search($query, $filters = [], $sort = 'relevance', $page = 1, $perPage = COURSES_PER_PAGE) {
$offset = ($page - 1) * $perPage;
$sql = "
SELECT c.*,
cat.name as category_name,
u.full_name as instructor_name,
MATCH(c.title, c.subtitle, c.description) AGAINST(:query) as relevance
FROM courses c
LEFT JOIN categories cat ON c.category_id = cat.id
LEFT JOIN users u ON c.instructor_id = u.id
WHERE c.is_published = 1 AND c.is_approved = 1
AND MATCH(c.title, c.subtitle, c.description) AGAINST(:query)
";
$params = ['query' => $query];
// Apply filters
if (!empty($filters['category'])) {
$sql .= " AND c.category_id = :category";
$params['category'] = $filters['category'];
}
if (!empty($filters['level'])) {
$sql .= " AND c.level = :level";
$params['level'] = $filters['level'];
}
if (!empty($filters['language'])) {
$sql .= " AND c.language = :language";
$params['language'] = $filters['language'];
}
if (!empty($filters['price'])) {
if ($filters['price'] == 'free') {
$sql .= " AND c.price = 0";
} elseif ($filters['price'] == 'paid') {
$sql .= " AND c.price > 0";
}
}
if (!empty($filters['min_price'])) {
$sql .= " AND c.price >= :min_price";
$params['min_price'] = $filters['min_price'];
}
if (!empty($filters['max_price'])) {
$sql .= " AND c.price <= :max_price";
$params['max_price'] = $filters['max_price'];
}
if (!empty($filters['rating'])) {
$sql .= " AND c.rating_avg >= :rating";
$params['rating'] = $filters['rating'];
}
// Apply sorting
switch ($sort) {
case 'price_low':
$sql .= " ORDER BY c.price ASC";
break;
case 'price_high':
$sql .= " ORDER BY c.price DESC";
break;
case 'newest':
$sql .= " ORDER BY c.created_at DESC";
break;
case 'popular':
$sql .= " ORDER BY c.students_count DESC";
break;
case 'rating':
$sql .= " ORDER BY c.rating_avg DESC";
break;
default:
$sql .= " ORDER BY relevance DESC, c.students_count DESC";
}
$sql .= " LIMIT :offset, :perPage";
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':query', $query);
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
$stmt->bindParam(':perPage', $perPage, PDO::PARAM_INT);
foreach ($params as $key => &$value) {
$stmt->bindParam($key, $value);
}
$stmt->execute();
return $stmt->fetchAll();
}
// Enroll user in course
public function enroll($userId, $courseId, $paymentId = null) {
// Check if already enrolled
$stmt = $this->pdo->prepare("SELECT id FROM enrollments WHERE user_id = ? AND course_id = ?");
$stmt->execute([$userId, $courseId]);
if ($stmt->fetch()) {
return ['success' => false, 'message' => 'Already enrolled'];
}
// Check prerequisites
$prereqCheck = checkPrerequisites($userId, $courseId);
if (!$prereqCheck['has_prerequisites']) {
return ['success' => false, 'message' => 'Prerequisites not met', 'prerequisites' => $prereqCheck['missing']];
}
// Create enrollment
$stmt = $this->pdo->prepare("
INSERT INTO enrollments (user_id, course_id)
VALUES (?, ?)
");
if ($stmt->execute([$userId, $courseId])) {
$enrollmentId = $this->pdo->lastInsertId();
// Update student count
$this->pdo->prepare("
UPDATE courses SET students_count = students_count + 1 WHERE id = ?
")->execute([$courseId]);
// Log interaction
logUserInteraction($userId, $courseId, 'enroll', ['enrollment_id' => $enrollmentId]);
return ['success' => true, 'enrollment_id' => $enrollmentId];
}
return ['success' => false, 'message' => 'Enrollment failed'];
}
// Get user enrollment
public function getEnrollment($userId, $courseId) {
$stmt = $this->pdo->prepare("
SELECT * FROM enrollments
WHERE user_id = ? AND course_id = ?
");
$stmt->execute([$userId, $courseId]);
return $stmt->fetch();
}
// Update lesson progress
public function updateProgress($enrollmentId, $lessonId, $completed = true, $position = 0) {
// Check if progress exists
$stmt = $this->pdo->prepare("
SELECT id FROM lesson_progress
WHERE enrollment_id = ? AND lesson_id = ?
");
$stmt->execute([$enrollmentId, $lessonId]);
$progress = $stmt->fetch();
if ($progress) {
// Update
$stmt = $this->pdo->prepare("
UPDATE lesson_progress
SET is_completed = ?, video_position = ?, completed_at = ?
WHERE enrollment_id = ? AND lesson_id = ?
");
$completedAt = $completed ? date('Y-m-d H:i:s') : null;
$stmt->execute([$completed, $position, $completedAt, $enrollmentId, $lessonId]);
} else {
// Insert
$stmt = $this->pdo->prepare("
INSERT INTO lesson_progress (enrollment_id, lesson_id, is_completed, video_position, completed_at)
VALUES (?, ?, ?, ?, ?)
");
$completedAt = $completed ? date('Y-m-d H:i:s') : null;
$stmt->execute([$enrollmentId, $lessonId, $completed, $position, $completedAt]);
}
// Calculate overall progress
$progress = calculateProgress($enrollmentId);
// Update enrollment
$stmt = $this->pdo->prepare("
UPDATE enrollments
SET progress = ?, last_accessed = NOW()
WHERE id = ?
");
$stmt->execute([$progress, $enrollmentId]);
// Check if course is completed
if ($progress == 100) {
$this->completeCourse($enrollmentId);
}
return $progress;
}
// Complete course
public function completeCourse($enrollmentId) {
// Get enrollment details
$stmt = $this->pdo->prepare("
SELECT * FROM enrollments WHERE id = ?
");
$stmt->execute([$enrollmentId]);
$enrollment = $stmt->fetch();
if (!$enrollment || $enrollment['is_completed']) {
return false;
}
// Update enrollment
$stmt = $this->pdo->prepare("
UPDATE enrollments
SET is_completed = 1, completed_at = NOW(), progress = 100
WHERE id = ?
");
$stmt->execute([$enrollmentId]);
// Generate certificate
$certNumber = generateCertificateNumber($enrollment['user_id'], $enrollment['course_id']);
// In a real app, you'd generate a PDF certificate
$certUrl = '/certificates/' . $certNumber . '.pdf';
$stmt = $this->pdo->prepare("
INSERT INTO certificates (user_id, course_id, certificate_number, certificate_url)
VALUES (?, ?, ?, ?)
");
$stmt->execute([$enrollment['user_id'], $enrollment['course_id'], $certNumber, $certUrl]);
// Log interaction
logUserInteraction($enrollment['user_id'], $enrollment['course_id'], 'complete');
return true;
}
// Add course review
public function addReview($userId, $courseId, $rating, $title, $content) {
// Check if already reviewed
$stmt = $this->pdo->prepare("SELECT id FROM course_reviews WHERE user_id = ? AND course_id = ?");
$stmt->execute([$userId, $courseId]);
if ($stmt->fetch()) {
return ['success' => false, 'message' => 'You have already reviewed this course'];
}
// Check if enrolled and completed
$stmt = $this->pdo->prepare("
SELECT id FROM enrollments
WHERE user_id = ? AND course_id = ? AND is_completed = 1
");
$stmt->execute([$userId, $courseId]);
if (!$stmt->fetch()) {
return ['success' => false, 'message' => 'You must complete the course to review it'];
}
// Insert review
$stmt = $this->pdo->prepare("
INSERT INTO course_reviews (user_id, course_id, rating, title, content)
VALUES (?, ?, ?, ?, ?)
");
if ($stmt->execute([$userId, $courseId, $rating, $title, $content])) {
// Update course rating
$this->updateCourseRating($courseId);
// Log interaction
logUserInteraction($userId, $courseId, 'review', ['rating' => $rating]);
return ['success' => true, 'review_id' => $this->pdo->lastInsertId()];
}
return ['success' => false, 'message' => 'Failed to add review'];
}
// Update course rating
private function updateCourseRating($courseId) {
$stmt = $this->pdo->prepare("
UPDATE courses
SET rating_avg = (SELECT AVG(rating) FROM course_reviews WHERE course_id = ? AND is_approved = 1),
review_count = (SELECT COUNT(*) FROM course_reviews WHERE course_id = ? AND is_approved = 1)
WHERE id = ?
");
$stmt->execute([$courseId, $courseId, $courseId]);
}
// Add to wishlist
public function addToWishlist($userId, $courseId) {
$stmt = $this->pdo->prepare("
INSERT IGNORE INTO wishlists (user_id, course_id)
VALUES (?, ?)
");
if ($stmt->execute([$userId, $courseId])) {
logUserInteraction($userId, $courseId, 'wishlist');
return true;
}
return false;
}
// Remove from wishlist
public function removeFromWishlist($userId, $courseId) {
$stmt = $this->pdo->prepare("
DELETE FROM wishlists WHERE user_id = ? AND course_id = ?
");
return $stmt->execute([$userId, $courseId]);
}
// Get user's wishlist
public function getWishlist($userId) {
$stmt = $this->pdo->prepare("
SELECT c.*, w.created_at as added_at
FROM wishlists w
JOIN courses c ON w.course_id = c.id
WHERE w.user_id = ?
ORDER BY w.created_at DESC
");
$stmt->execute([$userId]);
return $stmt->fetchAll();
}
// Get user's enrolled courses
public function getEnrolledCourses($userId) {
$stmt = $this->pdo->prepare("
SELECT c.*, e.progress, e.enrolled_at, e.is_completed, e.completed_at
FROM enrollments e
JOIN courses c ON e.course_id = c.id
WHERE e.user_id = ?
ORDER BY e.last_accessed DESC
");
$stmt->execute([$userId]);
return $stmt->fetchAll();
}
}
API ENDPOINTS
api/courses.php
<?php
require_once '../config.php';
$action = $_GET['action'] ?? '';
switch ($action) {
case 'get':
$id = $_GET['id'] ?? 0;
$slug = $_GET['slug'] ?? '';
$course = new Course($pdo);
if ($id) {
$data = $course->getById($id);
} elseif ($slug) {
$data = $course->getBySlug($slug);
} else {
jsonResponse(['success' => false, 'message' => 'Course ID or slug required'], 400);
}
if ($data) {
jsonResponse(['success' => true, 'course' => $data]);
} else {
jsonResponse(['success' => false, 'message' => 'Course not found'], 404);
}
break;
case 'search':
$query = $_GET['q'] ?? '';
$page = $_GET['page'] ?? 1;
$sort = $_GET['sort'] ?? 'relevance';
$filters = [
'category' => $_GET['category'] ?? null,
'level' => $_GET['level'] ?? null,
'language' => $_GET['language'] ?? null,
'price' => $_GET['price'] ?? null,
'min_price' => $_GET['min_price'] ?? null,
'max_price' => $_GET['max_price'] ?? null,
'rating' => $_GET['rating'] ?? null
];
$course = new Course($pdo);
$results = $course->search($query, $filters, $sort, $page);
jsonResponse(['success' => true, 'courses' => $results]);
break;
case 'enroll':
if (!isLoggedIn()) {
jsonResponse(['success' => false, 'message' => 'Not authenticated'], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
$course = new Course($pdo);
$result = $course->enroll(getCurrentUserId(), $data['course_id'], $data['payment_id'] ?? null);
jsonResponse($result);
break;
case 'progress':
if (!isLoggedIn()) {
jsonResponse(['success' => false, 'message' => 'Not authenticated'], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
$course = new Course($pdo);
$progress = $course->updateProgress(
$data['enrollment_id'],
$data['lesson_id'],
$data['completed'] ?? true,
$data['position'] ?? 0
);
jsonResponse(['success' => true, 'progress' => $progress]);
break;
case 'review':
if (!isLoggedIn()) {
jsonResponse(['success' => false, 'message' => 'Not authenticated'], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
$course = new Course($pdo);
$result = $course->addReview(
getCurrentUserId(),
$data['course_id'],
$data['rating'],
$data['title'],
$data['content']
);
jsonResponse($result);
break;
case 'wishlist':
if (!isLoggedIn()) {
jsonResponse(['success' => false, 'message' => 'Not authenticated'], 401);
}
$course = new Course($pdo);
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$wishlist = $course->getWishlist(getCurrentUserId());
jsonResponse(['success' => true, 'wishlist' => $wishlist]);
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
$result = $course->addToWishlist(getCurrentUserId(), $data['course_id']);
jsonResponse(['success' => $result]);
} elseif ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
$data = json_decode(file_get_contents('php://input'), true);
$result = $course->removeFromWishlist(getCurrentUserId(), $data['course_id']);
jsonResponse(['success' => $result]);
}
break;
case 'enrolled':
if (!isLoggedIn()) {
jsonResponse(['success' => false, 'message' => 'Not authenticated'], 401);
}
$course = new Course($pdo);
$courses = $course->getEnrolledCourses(getCurrentUserId());
jsonResponse(['success' => true, 'courses' => $courses]);
break;
default:
jsonResponse(['success' => false, 'message' => 'Invalid action'], 400);
}
?>
api/recommendations.php
<?php
require_once '../config.php';
$action = $_GET['action'] ?? 'personalized';
$userId = getCurrentUserId() ?? 0;
$recommender = new CourseRecommender($pdo);
switch ($action) {
case 'personalized':
if (!$userId) {
// Not logged in, show popular courses
$courses = $recommender->getPopularCourses($_GET['limit'] ?? RECOMMENDATION_LIMIT);
jsonResponse(['success' => true, 'recommendations' => $courses]);
} else {
$recommendations = $recommender->getRecommendations($userId, 'personalized', $_GET['limit'] ?? RECOMMENDATION_LIMIT);
jsonResponse(['success' => true, 'recommendations' => $recommendations]);
}
break;
case 'trending':
$recommendations = $recommender->getTrendingCourses($_GET['limit'] ?? RECOMMENDATION_LIMIT);
jsonResponse(['success' => true, 'recommendations' => $recommendations]);
break;
case 'popular':
$recommendations = $recommender->getPopularCourses($_GET['limit'] ?? RECOMMENDATION_LIMIT);
jsonResponse(['success' => true, 'recommendations' => $recommendations]);
break;
case 'new':
$recommendations = $recommender->getNewCourses($_GET['limit'] ?? RECOMMENDATION_LIMIT);
jsonResponse(['success' => true, 'recommendations' => $recommendations]);
break;
case 'skill_gap':
if (!$userId) {
jsonResponse(['success' => false, 'message' => 'Not authenticated'], 401);
}
$recommendations = $recommender->getSkillGapRecommendations($userId, $_GET['limit'] ?? RECOMMENDATION_LIMIT);
jsonResponse(['success' => true, 'recommendations' => $recommendations]);
break;
case 'similar':
$courseId = $_GET['course_id'] ?? 0;
if (!$courseId) {
jsonResponse(['success' => false, 'message' => 'Course ID required'], 400);
}
$recommendations = $recommender->getSimilarCourses($courseId, $_GET['limit'] ?? 10);
jsonResponse(['success' => true, 'recommendations' => $recommendations]);
break;
case 'learning_path':
if (!$userId) {
jsonResponse(['success' => false, 'message' => 'Not authenticated'], 401);
}
$recommendations = $recommender->getLearningPathRecommendations($userId, $_GET['limit'] ?? RECOMMENDATION_LIMIT);
jsonResponse(['success' => true, 'recommendations' => $recommendations]);
break;
case 'category':
$categoryId = $_GET['category_id'] ?? 0;
if (!$categoryId) {
jsonResponse(['success' => false, 'message' => 'Category ID required'], 400);
}
if (!$userId) {
$recommendations = $recommender->getCategoryRecommendations(0, $categoryId, $_GET['limit'] ?? RECOMMENDATION_LIMIT);
} else {
$recommendations = $recommender->getCategoryRecommendations($userId, $categoryId, $_GET['limit'] ?? RECOMMENDATION_LIMIT);
}
jsonResponse(['success' => true, 'recommendations' => $recommendations]);
break;
default:
jsonResponse(['success' => false, 'message' => 'Invalid action'], 400);
}
?>
PROJECT STRUCTURE
lms-platform/ │ ├── index.html # Main landing page ├── course.html # Course detail page ├── dashboard.html # Student dashboard ├── instructor.html # Instructor dashboard ├── admin.html # Admin dashboard ├── search.html # Search results ├── profile.html # User profile ├── checkout.html # Checkout page ├── lesson.html # Lesson player │ ├── config.php # Database configuration ├── helpers.php # Helper functions │ ├── classes/ │ ├── CourseRecommender.php # Recommendation engine │ ├── Course.php # Course management │ └── User.php # User management │ ├── api/ │ ├── auth.php # Authentication endpoints │ ├── courses.php # Course endpoints │ ├── recommendations.php # Recommendation endpoints │ ├── users.php # User endpoints │ └── payments.php # Payment endpoints │ ├── assets/ │ ├── images/ │ │ ├── course-placeholder.jpg │ │ └── default-avatar.png │ └── css/ │ └── style.css │ └── uploads/ ├── courses/ ├── lessons/ ├── assignments/ ├── profiles/ └── certificates/
FEATURES SUMMARY
✅ Student Features
- Course browsing & search
- Course enrollment
- Video lessons with progress tracking
- Quizzes and assignments
- Notes and bookmarks
- Course completion certificates
- Discussion forums
- Personal dashboard
✅ Instructor Features
- Course creation wizard
- Lesson management
- Quiz builder
- Assignment grading
- Student analytics
- Earnings dashboard
- Discussion moderation
✅ Admin Features
- User management
- Course approval
- Category management
- Platform analytics
- Payment processing
- Report generation
✅ Recommendation System
- Personalized course recommendations
- Skill gap analysis
- Learning path suggestions
- Similar courses
- Trending and popular courses
- Category-based suggestions
- Prerequisite-based recommendations
✅ Recommendation Algorithms
- Content-based filtering
- Collaborative filtering
- Skill-based matching
- Learning path optimization
- Popularity scoring
- Recency weighting
- Category affinity
This comprehensive Learning Management System includes a sophisticated recommendation engine that analyzes user skills, learning history, and career goals to provide personalized course suggestions and learning paths. The system helps students discover relevant content and achieve their educational objectives efficiently.