Developing an Educational Platform (LMS) on 1C-Bitrix
1C-Bitrix is not the first choice for an LMS. There is Moodle, Teachable, GetCourse. But companies choose Bitrix when they already have a site on it, need unified authentication with a corporate portal or online store, and have no desire to maintain multiple systems. In that case Bitrix handles it well enough: info blocks for storing courses, user groups for access control, agents for progress, and the payment module for monetisation.
Data Storage Architecture
An LMS on Bitrix is built on a combination of info blocks and HighLoad blocks (HL blocks, tables via D7 ORM).
Info blocks:
| Info block | Code | Purpose |
|---|---|---|
| Courses | lms_courses |
Main course entities |
| Lessons | lms_lessons |
Lessons within a course |
| Quizzes | lms_quizzes |
Assignments and tests |
HL blocks (via ORM):
// User progress
class UserProgressTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'b_hl_lms_user_progress'; }
public static function getMap(): array
{
return [
new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new IntegerField('USER_ID'),
new IntegerField('COURSE_ID'), // Info block course element ID
new IntegerField('LESSON_ID'), // Info block lesson element ID
new EnumField('STATUS', ['values' => ['NOT_STARTED', 'IN_PROGRESS', 'COMPLETED']]),
new IntegerField('PROGRESS_PERCENT'), // 0–100
new DatetimeField('STARTED_AT'),
new DatetimeField('COMPLETED_AT'),
new IntegerField('TIME_SPENT_SEC'), // Time spent on the lesson
];
}
}
// Course enrolment
class CourseEnrollmentTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'b_hl_lms_enrollment'; }
public static function getMap(): array
{
return [
new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new IntegerField('USER_ID'),
new IntegerField('COURSE_ID'),
new EnumField('STATUS', ['values' => ['ACTIVE', 'COMPLETED', 'EXPIRED', 'REFUNDED']]),
new DatetimeField('ENROLLED_AT'),
new DatetimeField('EXPIRES_AT'), // NULL = unlimited
new FloatField('PAID_AMOUNT'),
new IntegerField('ORDER_ID'), // Link to sale module order
];
}
}
Course and Lesson Structure
A course is an element of the lms_courses info block with properties:
| Property | Code | Type |
|---|---|---|
| Category | CATEGORY |
List |
| Duration | DURATION_HOURS |
Number |
| Level | LEVEL |
List (Beginner/Intermediate/Expert) |
| Access type | ACCESS_TYPE |
List (Free/Paid/Subscription) |
| Access group | ACCESS_GROUP_ID |
Number (b_group ID) |
| Certificate | HAS_CERTIFICATE |
Flag |
A lesson is an element of the lms_lessons info block, linked to a course via the COURSE_ID property. Lesson order — via SORT. Lesson content type (LESSON_TYPE): Video / Text / Quiz / Webinar.
Course Access
Access is controlled through Bitrix user groups. Each course has its own group (b_group). After payment or enrolment the user is added to the group:
class EnrollmentService
{
public function enroll(int $userId, int $courseId): void
{
$course = \CIBlockElement::GetByID($courseId)->GetNext();
$groupId = (int)$course['PROPERTY_ACCESS_GROUP_ID_VALUE'];
if ($groupId) {
$currentGroups = \CUser::GetUserGroup($userId);
if (!in_array($groupId, $currentGroups)) {
$currentGroups[] = $groupId;
\CUser::SetUserGroup($userId, $currentGroups);
}
}
CourseEnrollmentTable::add([
'USER_ID' => $userId,
'COURSE_ID' => $courseId,
'STATUS' => 'ACTIVE',
'ENROLLED_AT' => new \Bitrix\Main\Type\DateTime(),
]);
}
}
Access to course pages is restricted at the PHP level:
// At the start of the lesson page
$enrollment = CourseEnrollmentTable::getList([
'filter' => [
'USER_ID' => $USER->GetID(),
'COURSE_ID' => $courseId,
'STATUS' => 'ACTIVE',
],
])->fetch();
if (!$enrollment) {
LocalRedirect('/courses/' . $courseSlug . '/buy/');
}
Progress Tracking
Progress is recorded under several conditions depending on the lesson type:
-
Video: tracking via the player's
timeupdateevent; marked as "watched" when 80% of the video has been completed. - Text: marked when the user scrolls to the bottom of the page (IntersectionObserver on the last paragraph).
- Quiz: upon successful completion (score ≥ passing threshold).
// AJAX endpoint for recording progress
// POST /local/ajax/lms_progress.php
$lessonId = (int)$_POST['lesson_id'];
$courseId = (int)$_POST['course_id'];
$userId = $USER->GetID();
$percent = min(100, (int)$_POST['percent']);
$existing = UserProgressTable::getList([
'filter' => ['USER_ID' => $userId, 'LESSON_ID' => $lessonId],
])->fetch();
if ($existing) {
if ($percent > $existing['PROGRESS_PERCENT']) {
UserProgressTable::update($existing['ID'], [
'PROGRESS_PERCENT' => $percent,
'STATUS' => $percent >= 100 ? 'COMPLETED' : 'IN_PROGRESS',
'COMPLETED_AT' => $percent >= 100 ? new \Bitrix\Main\Type\DateTime() : null,
]);
}
} else {
UserProgressTable::add([
'USER_ID' => $userId,
'COURSE_ID' => $courseId,
'LESSON_ID' => $lessonId,
'STATUS' => $percent >= 100 ? 'COMPLETED' : 'IN_PROGRESS',
'PROGRESS_PERCENT' => $percent,
'STARTED_AT' => new \Bitrix\Main\Type\DateTime(),
'COMPLETED_AT' => $percent >= 100 ? new \Bitrix\Main\Type\DateTime() : null,
]);
}
Testing and Quizzes
Quiz questions are elements of the lms_quizzes info block with properties: question type (single choice, multiple choice, text answer), answer options (JSON in a string property), correct answer, and question weight.
Quiz results — HL block b_hl_lms_quiz_attempt:
CREATE TABLE b_hl_lms_quiz_attempt (
ID SERIAL PRIMARY KEY,
USER_ID INTEGER,
QUIZ_ID INTEGER,
SCORE INTEGER, -- points scored
MAX_SCORE INTEGER, -- maximum points
PASSED BOOLEAN,
ANSWERS_JSON TEXT, -- JSON with user answers
CREATED_AT TIMESTAMP
);
Certificates
Upon course completion (all lessons COMPLETED) — generate a PDF certificate using the FPDF or TCPDF library:
class CertificateGenerator
{
public function generate(int $userId, int $courseId): string
{
$user = \CUser::GetByID($userId)->Fetch();
$course = \CIBlockElement::GetByID($courseId)->GetNext();
$pdf = new \TCPDF();
$pdf->AddPage('L'); // Landscape
$pdf->setImageScale(1.25);
// Certificate background image
$pdf->Image('/local/templates/lms/img/certificate_bg.jpg', 0, 0, 297, 210);
// Full name
$pdf->SetFont('dejavusans', 'B', 32);
$pdf->SetXY(50, 90);
$pdf->Cell(200, 20, $user['LAST_NAME'] . ' ' . $user['NAME'], 0, 0, 'C');
// Course name
$pdf->SetFont('dejavusans', '', 18);
$pdf->SetXY(50, 120);
$pdf->Cell(200, 10, $course['NAME'], 0, 0, 'C');
$filename = 'certificate_' . $userId . '_' . $courseId . '.pdf';
$path = '/upload/certificates/' . $filename;
$pdf->Output($_SERVER['DOCUMENT_ROOT'] . $path, 'F');
return $path;
}
}
Development Timelines
| Option | Composition | Duration |
|---|---|---|
| Basic LMS | Courses, lessons, progress, group access | 15–20 days |
| With quizzes and certificates | + Quizzes, PDF generation | 20–30 days |
| Full platform | + Payment, subscriptions, analytics, webinars | 35–50 days |







