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 carriescompany_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 throughstudent_lesson.
2.3 Quizzes & exams
OneQuiz 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)
Oneorders 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 aNotificationDispatch (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 vs exam: one model, two meanings
Quiz vs exam: one model, two meanings
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).Ownership by slug
Ownership by slug
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.Tenant isolation
Tenant isolation
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.Reviews and certificates target a training OR an exam
Reviews and certificates target a training OR an exam
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.One category per training, exam and quiz
One category per training, exam and quiz
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.Files
Files
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.
Code notes found while building this page (28/10/2026)
Code notes found while building this page (28/10/2026)
Company::quizzes(),reviews(),vouchers(),orders()andcertifications()returnhasMany(Category::class), a copy-paste leftover. The diagrams show the intended relations (toQuiz,Review,Voucher,Order,Certification); the controllers query those tables bycompany_iddirectly.QuizQuestion::students()points at astudent_quizzestable that no migration creates. It is left out of the diagrams.NotificationType::AI_GENERATED_IDandENROLLMENT_IDboth equal13.