escolalms/courses
Laravel package for managing courses in the eSkool LMS: define courses, attach lessons and content, handle enrollment, and expose course data through Eloquent models and APIs so you can build e-learning features into your PHP application.
This package (escolalms/courses) is the course-structure layer of the EscolaLMS/Wellms stack: it owns Course, lessons/topics, course authorship, course assignment to users, and the deadline that governs how long a learner has access. It sits on top of escolalms/core (users, auth, roles/permissions) and is normally consumed alongside escolalms/topic-types, escolalms/lessons-style content packages, and a frontend hitting the JSON:API-ish endpoints.
Minimal setup:
composer require escolalms/courses (expect escolalms/core and spatie/laravel-permission as transitive requirements — install/configure core first, since migrations reference the users table).php artisan vendor:publish --provider="EscolaLMS\Courses\EscolaLMSCoursesServiceProvider"php artisan migrateadmin, tutor, student style roles). Without them the API endpoints will 403 rather than fail loudly.auth:api / Passport or Sanctum depending on your core version) and hit the courses routes registered under the package's API prefix.Where to look first:
src/Models/ — Course and the assignment/pivot models. Read the casts and date columns before writing anything; deadline behaviour lives here.src/Events/ — CourseAssigned (and its siblings such as unassign/start events). Events are the intended integration seam, not direct writes.src/Listeners/ and the service provider's $listen array — this is where the 0.4.45 deadline recalculation lives conceptually.config/courses.php — table names, model bindings, and any feature toggles.src/Http/Controllers/API/ + src/Http/Resources/ — the contract your frontend will code against.First use case to validate your install: create a course with a duration/active window, assign it to a user, and confirm the resulting assignment row has a deadline. Then log in as that user and confirm the course appears with access gated correctly.
Assign through the package, never by inserting pivot rows. The whole point of the 0.4.45 fix is that CourseAssigned now carries deadline recalculation. Any code path that assigns a course must dispatch that event (usually just by using the package's assign action/service). Raw DB::table(...)->insert() or ->sync() on the pivot will produce assignments with stale or null deadlines — a silent bug that only shows up weeks later when learners lose access at the wrong time.
Treat the deadline as derived, not authored. Compute it from the course's duration/template and the assignment timestamp, and let the package own that calculation. If you need a custom rule (e.g. deadline = end of academic term, or extended for a cohort), implement it as a listener on CourseAssigned that adjusts after the package has done its work, using $event->course / $event->user rather than re-deriving from scratch. Queue that listener (ShouldQueue) if it touches external systems, but keep the recalculation itself synchronous so the API response reflects the final deadline.
Wrap assignment + enrolment side effects in one transaction boundary conceptually, not literally. The event fires after the write; if your listener throws, the assignment may already be persisted. Make listeners idempotent (look up the assignment, upsert the deadline) so retries from failed jobs don't compound.
Drive access control off the deadline. In your own controllers/policies, gate topic/lesson access on the assignment's deadline rather than on the course's static end date. A user re-assigned to a course should get a fresh deadline (this is precisely what changed), so caching a "course is active" boolean per user is a trap — key your cache on the assignment row and invalidate on CourseAssigned/CourseUnassigned.
Customise at the edges. Prefer:
->additional() overrides in your own controller layer,app/.Forking is the last resort — EscolaLMS packages move fast in the 0.4.x line and merge conflicts in src/Models/Course.php are painful.
Testing pattern. For feature tests around enrolment, assert on the event (Event::assertDispatched(CourseAssigned::class)) and on the persisted deadline. For unit tests of your own listeners, call them directly with a hand-built event so you don't depend on the package's dispatch order.
^0.4 safely. But if you have a workaround that recalculated deadlines manually on assignment, remove it now — double recalculation can shift a deadline forward on every re-assignment.CourseAssigned, assigning an already-assigned user (e.g. an idempotent admin action or a CSV import re-run) may reset their deadline. Decide whether that's desired; if not, guard your import so it only assigns when no active assignment exists.Model::insert(), query-builder updates, and ->update(['deadline' => ...]) on the pivot bypass everything. If you import enrolments at scale, dispatch CourseAssigned per row (chunked + queued) or accept that you must backfill deadlines in a follow-up job.config/app.php; comparing against now() in a user's local timezone produces off-by-hours access errors around midnight. Always compare in the app timezone and let the frontend format.$dates/$casts on the assignment model before assuming a deadline is a Carbon instance — a raw string from a query builder read will happily strtotime() into the wrong thing.config:cache after upgrade. EscolaLMS config files are read at boot; after composer update on a cached deployment, php artisan config:clear (or rebuild the cache in your deploy script) to pick up new keys. Stale config is the usual cause of "the fix isn't working".php artisan permission:cache-reset after seeding.Event::fake() in a test silently disables the new recalculation and will make a bug look fixed. Run the failing scenario once with Event::fake() removed and DB::listen() or Log::info inside your listener to see the actual deadline written.$listen array is where you hook CourseAssigned → your own deadline policy, notification, or enrolment sync. Register listeners there (or in your own EventServiceProvider) rather than in the model's booted() method — model events don't fire for the package's internal service-level operations.How can I help you explore Laravel packages today?