escolalms/scorm
Laravel package for integrating SCORM e-learning content into your application, providing backend support for uploading, playing, and tracking SCORM packages and learner progress. Part of the EscolaLMS ecosystem for building online learning platforms.
Install and migrate.
composer require escolalms/scorm
php artisan vendor:publish --provider="EscolaLms\Scorm\Providers\ScormServiceProvider"
php artisan migrate
The package is part of the EscolaLMS ecosystem, so it expects escolalms/courses and escolalms/auth to already be installed. Confirm the exact service-provider class name from composer.json / src/Providers — it may be ScormServiceProvider or ScormServiceProvider.
Where to look first.
config/scorm.php — storage disk, allowed mime types, package size limit, route prefix/middleware.database/migrations/* — the scorms table and how it links to courses (likely course_id or a polymorphic topicable/model_id relationship, since EscolaLMS content is topic-based).routes/api.php — the upload, launch, and tracking endpoints.src/Models/ — the Scorm model and its relationships (course(), topic(), user progress).src/Http/Controllers/ — upload and "launch" controllers are the two you will touch most.First end-to-end use case. Attach a SCORM 1.2 ZIP to a course and play it:
escolalms/courses enrollment checks; without enrollment the launch/tracking endpoints will 403).POST /api/scorm or similar).entry_point, title, version).initialize/commit/terminate) land in the scorm_user_progress (or equivalently named) table, and confirm a completed status triggers the topic/course progress update.Upload → parse → attach flow. Use the package's upload endpoint (or dispatch its parsing job yourself if you have a separate admin UI). The ZIP is stored on the configured disk, then a job extracts it and parses imsmanifest.xml to derive the schema version (1.2 vs 2004), the entry file, and the organization title. Keep this asynchronous: a 50 MB package with hundreds of assets will blow past the default 30s HTTP timeout.
$scorm = Scorm::create([
'course_id' => $course->id,
'title' => $request->file('package')->getClientOriginalName(),
]);
$scorm->addMediaFromRequest('package')->toMediaCollection('package');
ScormParserJob::dispatch($scorm); // verify class name in src/Jobs
Launching content. Treat the launch response as a contract your frontend consumes: it should return the extracted package's base URL plus the entry_point from the manifest. Your player mounts that in an <iframe> and defines the SCORM API adapter on window before the iframe loads:
<script>
window.API = {
LMSInitialize: () => "true",
LMSGetValue: (k) => fetch(`/api/scorm/${id}/get?key=${k}`).then(...),
LMSSetValue: (k, v) => { pending[k] = v; return "true"; },
LMSCommit: () => postCommit(pending),
LMSFinish: () => postCommit(pending, true),
LMSGetLastError: () => "0",
LMSGetErrorString: () => "",
LMSGetDiagnostic: () => "",
};
</script>
<iframe src="{{ $launchUrl }}" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
Tracking and completion. The commit and terminate endpoints are the heart of the integration. Buffer cmi.* writes on the client and flush them on LMSSetValue batches to avoid one request per field. On terminate, map cmi.core.lesson_status (1.2) or cmi.completion_status + cmi.success_status (2004) onto your course progress model. If your app listens to an EscolaLMS event (TopicCompleted, CourseProgressUpdated), hook into it here rather than duplicating logic in a controller.
Authorization. Every launch and tracking call must re-check enrollment and that the requesting user owns the progress row. Do not trust a user_id in the payload — derive it from Auth::user(), or tracking gets forgeable by any authenticated learner.
Integration tips.
imsmanifest.xml-relative navigation.saved, deleted) to cascade cleanup of extracted assets, otherwise orphaned directories accumulate on disk.window.API; SCORM 2004 expects window.API_1484_11 with different method names (Initialize, GetValue, SetValue, Commit, Terminate). Serving a 1.2 adapter to a 2004 package silently disables tracking. Confirm the package parses schemaversion from the manifest and lets the player pick the adapter.../ or absolute paths (Zip Slip). If the package doesn't, add your own validation before dispatch — ZipArchive::extractTo follows paths blindly.imsmanifest.xml namespaces. Parsing with SimpleXML requires registering the SCORM namespaces (http://www.imsproject.org/xsd/imscp_rootv1p1p2, etc.); a naive $xml->organizations->organization query returns empty. Check the parser handles both namespace-qualified and bare queries.upload_max_filesize, post_max_size, and max_execution_time, and set the queue timeout on the parse job; the default 60s worker timeout is a common silent failure.storage/app/... path is either linked to public/ or served through a controller — a 404 on the entry point is almost always permissions or a wrong disk.allow-scripts alone blocks the SCORM API because the content can't reach the parent. Use allow-scripts allow-same-origin (or serve from the same origin) if you need the adapter injected via window.parent.API.lesson_status can be not attempted, incomplete, completed, passed, failed. Do not collapse passed/completed into one flag — courses that gate on success will be wrong. For 2004, completion_status and success_status are separate.cmi data is stored as a JSON blob and updated on every commit. Prune or truncate on terminate with the package's pruning command (check src/Console); otherwise the table balloons.Scorm record rather than replacing. Old progress rows won't migrate, so version packages explicitly and keep the scorm_id on progress rows stable if you care about resuming learners mid-course.index.html SCO). Commit these to tests/fixtures and run them through the full launch → initialize → set → finish → verify-progress path in a feature test.php artisan route:cache in production, verify the package's routes are registered via the service provider and not a bootstrap file, or your SCORM endpoints vanish after cache warmup.How can I help you explore Laravel packages today?