eren/lms
eren/lms is a Laravel package for adding learning management features to your app: courses, lessons, enrollments, progress tracking, and quizzes. It includes migrations, Eloquent models, and configurable routes to get a simple LMS running quickly.
Confirm distribution first: v1.8 is a small PR-and-dependabot release with no changelog detail, so verify whether the package is on Packagist or must be pulled from the repo directly.
composer require noumanahmad448/lms # or add a vcs repository + "dev-main"/"^1.8"
php artisan vendor:publish --provider="NoumanAhmad448\Lms\LmsServiceProvider"
php artisan migrate
Look at, in this order:
config/lms.php (or whatever the published config is named) — this is where table prefixes, model overrides, and route middleware are almost always exposed.routes/ in the package source — the route names and middleware groups reveal how much authorization is already enforced versus left to you.The first thing to build: a course with one video lesson and one enrolled user, end to end. The v1.8 diff bumps dplayer, which strongly implies lesson playback is a first-class part of the package — so proving the video path renders and records progress is the highest-value smoke test. Do that before touching your real domain.
Gate everything through enrollment. Treat the package's course/lesson routes as untrusted until you've read the middleware stack. The standard pattern is a policy or a small middleware that resolves Course from the route and checks an enrollments record for auth()->user():
Route::middleware(['auth', 'enrolled'])->group(function () {
Route::get('/courses/{course}/lessons/{lesson}', [LessonController::class, 'show']);
});
Wrap authoring in its own policy. Instructor-facing CRUD (create course, add lesson, reorder) should sit behind a CoursePolicy with create/update/delete methods, not behind a role string check scattered across controllers. If the package ships its own controllers, override the model via the config's model map rather than forking.
Keep progress writes idempotent. Video progress is the classic source of duplicate rows — the player fires timeupdate/completion repeatedly. Use updateOrCreate keyed on (user_id, lesson_id), or a dedicated markComplete() action, and debounce on the client:
axios.post(`/api/lessons/${lessonId}/progress`, { position: seconds })
.catch(() => {/* swallow — never block playback */});
Serve video through signed URLs, not public disk paths. If lessons store a path on the public disk, swap to Storage::disk('local')->temporaryUrl(...) (or a streaming controller) so unenrolled users can't guess URLs. This matters more than anything else in the package for a paid-course use case.
Push side effects into events. Enrollment created, lesson completed, course finished — model these as events with queued listeners for emails, certificate generation, and external LMS/webhook sync. That keeps the package's write path thin and makes your customizations survive upgrades.
Headless usage. If you're building a SPA or mobile client, expose the package models through your own API resources rather than serializing them directly — you control the shape, and the package's toArray() won't leak internal columns when it changes.
axios, dplayer) plus a maintainer PR. There is no feature or breaking-change signal in the notes. Before upgrading, diff PR #30 and PR #28 — a major-version bump of either library is the realistic breakage vector, and the release notes won't tell you.public/vendor/lms, a dplayer bump means the shipped JS changed. Run vendor:publish --force --tag=lms-assets (check the tag) and clear the view/asset cache; otherwise you're serving the old player against new backend expectations or vice versa. Also wipe node_modules if you compile the package's JS yourself — axios major bumps have historically changed default withCredentials, interceptor ordering, and CSRF header handling, which surfaces as mysterious 419s.vendor:publish defaults to not overwriting, so after an upgrade you can end up with stale migrations that migrate then re-runs under new filenames. Diff the package's database/migrations against yours before publishing again.auth and enrollment checks; a missing can: on a lesson route is a content-leak bug, not a UX bug.Course::withCount('lessons') and eager-loading enrollments for the index page is the difference between 3 queries and 300. Enable Model::preventLazyLoading() in local dev to catch it.DB::listen() or a Log::info in the enrollment path rather than guessing; php artisan tinker against the package models is faster than reading source for behavior questions."^1.8" with a committed composer.lock and read the compare diff before any minor bump that touches JS dependencies.How can I help you explore Laravel packages today?