Skip to main content
This page maps the 43 Eloquent models in LMS_Backend/app/Models and the relations they declare (belongsTo, hasMany, hasOne, belongsToMany). It was built from the backend code on 28/10/2026. Table of Contents
  • How to read the diagrams
  • 1. Global class diagram
  • 2. Domain views: tenancy & access, catalog & content, quizzes & exams, commerce, classrooms & chatrooms, notifications, landing page & settings
  • 3. Domain rules the diagrams don’t show
  • 4. Pivot and support tables without a model
  • 5. Keeping this page in sync

How to read the diagrams

Every model also has id, created_at and updated_at; the diagrams leave them out. Attributes are the $fillable columns (plus the important $casts), with the PHP cast as the type.

1. Global class diagram

All models and all relations, with only the key attributes. Use the zoom controls, or jump to the domain views in §2 for the full attribute lists.
Many models also point at File for their images and videos (image_id, video_id, badge_logo_id, company_image_id, signature_id, cachet_id, header_image_id, …). The global diagram leaves those arrows out to stay readable; each domain view in §2 shows them.

2. Domain views

Each view shows the full attribute list and the main methods of the models in one domain. Classes from another domain appear with an empty body.

2.1 Tenancy, users & access

A company is the tenant: almost every table carries company_id. A user has exactly one platform role (users.role_id), and what that role may do is set per company in permission_role.

2.2 Catalog & content (trainings, chapters, lessons)

A training is a course: chapters hold lessons, and a chapter can hold several quizzes. Students mark lessons as done through student_lesson.

2.3 Quizzes & exams

One Quiz model covers both: a quiz is attached to a training or a chapter, an exam is standalone and sold on its own (see §3).

2.4 Commerce (orders, coupons, vouchers, payment)

One orders table holds a student’s wishlist (type = 1), cart (type = 2) and real orders (type = 3). The items live in order_items (a training or an exam, with its price and access_revoked). See Orders & payment, Coupons and Vouchers.

2.5 Classrooms & chatrooms

A classroom groups students around an instructor. Its chatroom shares the classroom’s slug but is its own entity with its own four tables (see Chatrooms).

2.6 Notifications

A super admin’s broadcast is stored once as a NotificationDispatch (who it targets, when to send). When it runs, it writes one Notification row per target user.

2.7 Landing page & platform settings


3. Domain rules the diagrams don’t show

Quiz is both. When training_id or chapter_id is set, it is a quiz inside a training (isQuiz(), scope isQuiz). When both are null, it is a standalone exam (isExam(), scope isExam) with its own price, instructor, categories, reviews and certificates, sold through order_items like a training. A training can have several chapter quizzes; Training::quiz() returns the training’s own final quiz (no chapter_id).
Besides the paid order_items, a user’s access is cached in JSON arrays on the users row: owned_trainings and owned_exams (slugs), classroom_joined_slugs and classroom_admin_slugs. User::grantOwnedAssetsFromOrder() fills them when an order is paid, and sanitizeOwnedAssets() cleans them. These arrays are not foreign keys, so the diagrams show them as attributes only.
Every company-owned model carries company_id (trainings, quizzes, orders, coupons, vouchers, files, notifications, chatrooms, settings…). Policies and queries always filter on the caller’s company; only the super admin crosses companies.
Review and Certification both hold a nullable training_id and a nullable quiz_id: exactly one is set. A certificate’s public identifier is certification_id, generated on create.
Since 01/11/2026 a training, an exam or a quiz has at most one category. Training::categories() and Quiz::categories() are still belongsToMany through category_training / category_quiz (and the API keeps the categories array for older app builds), but the migration 2026_11_01_000000_keep_one_category_per_item added a unique index on training_id / quiz_id, so a second attach() fails; change a category with sync([$id]). The limit is Category::MAX_PER_ITEM. Tags stay many-to-many. See Trainings → Assign categories and Quizzes & Exams §3.3.
File.type says where the file lives: 1 internal disk, 2 external URL, 3 Cloudflare R2. Images, videos, logos, signatures and stamps are all File rows referenced by an *_id column.

4. Pivot and support tables without a model


5. Keeping this page in sync

When a backend change adds, removes or renames a model, a relation method or a $fillable column in app/Models, update the class in the matching §2 view and in the global diagram of §1.
  • Company::quizzes(), reviews(), vouchers(), orders() and certifications() return hasMany(Category::class), a copy-paste leftover. The diagrams show the intended relations (to Quiz, Review, Voucher, Order, Certification); the controllers query those tables by company_id directly.
  • QuizQuestion::students() points at a student_quizzes table that no migration creates. It is left out of the diagrams.
  • NotificationType::AI_GENERATED_ID and ENROLLMENT_ID both equal 13.