jorisdugue/h5p-bundle
Laravel bundle for integrating H5P interactive content into PHP applications. Manage H5P libraries, content types, and editor/viewer assets, making it easier to create, embed, and serve rich interactive exercises and quizzes.
What this package actually is. jorisdugue/h5p-bundle is a Symfony bundle that wires the official h5p/h5p-core and h5p/h5p-editor PHP libraries into a Symfony app: service definitions, routes/controllers for the editor and AJAX endpoints, Doctrine entities for content/libraries, and a filesystem abstraction for the h5p/ directory tree. There is no Laravel equivalent here — as a Laravel developer you have three realistic options:
h5p/h5p-core + h5p/h5p-editor directly and reimplement what the bundle does (container bindings, routes, storage, migrations) as a Laravel package. The bundle is a good reference implementation for this.H5PStandalone player if you only need playback, not authoring.Where to look first (in vendor/jorisdugue/h5p-bundle):
| Path | Why |
|---|---|
src/DependencyInjection/Configuration.php |
The full config surface (h5p_bundle: keys) — storage paths, public URLs, DB, assets |
src/Resources/config/ |
Service wiring; shows which H5P core classes are constructed and with what |
src/Controller/ |
The route contract: editor, ajax, content, library upload/download |
src/Entity/ |
The Doctrine schema you'd translate to Laravel migrations |
README.md |
Install order (bundle registration → routes → config → migrations → assets) |
Minimal steps in a Symfony host:
composer require jorisdugue/h5p-bundle
// config/bundles.php
Jorisdugue\H5PBundle\H5PBundle::class => ['all' => true],
# config/routes.yaml
h5p:
resource: '@H5PBundle/Resources/config/routes.yaml'
prefix: /h5p
# config/packages/h5p_bundle.yaml
h5p_bundle:
storage:
path: '%kernel.project_dir%/var/h5p' # libraries/, content/, files/, cachedassets/
public_url: '/h5p-assets' # MUST be web-served
database:
driver: doctrine
Then: run the migrations that create h5p_content, h5p_libraries, h5p_libraries_libraries, h5p_libraries_language, h5p_content_dependencies, h5p_content_user_data; symlink (or route-serve) var/h5p to public/h5p-assets; bin/console assets:install.
First use case. Log into the admin area, hit the bundle's editor route, upload a content type .h5p library (e.g. Course Presentation or Interactive Video), create one piece of content, then embed it:
{{-- Laravel / Blade side --}}
<div class="h5p-content" data-content-id="{{ $h5pContentId }}"></div>
<script src="https://your-symfony-host/h5p-assets/h5p-core/js/h5p.js"></script>
<script>
H5PIntegration = {!! $integrationJson !!}; // returned by the host's content endpoint
</script>
That "create a quiz in the editor, embed via H5PIntegration" loop is the whole day-to-day of this package.
1. Editor vs. player must be separated. The editor routes (/h5p/editor/*, library upload) are privileged — a user who can reach them can upload PHP-bearing .h5p packages. Put them behind your admin firewall (ROLE_ADMIN, or a dedicated H5P author role via the bundle's voter if present). Player routes (/h5p/content/{id}, /h5p/ajax) only need to be reachable by learners. In the sidecar model, expose the player route publicly and the editor route only on your internal network.
2. Storage abstraction is the seam you'll actually touch. H5P writes four sibling directories under one root: libraries/, content/, files/, cachedassets/. Configure a single Flysystem adapter and make sure the same root is reachable by both PHP (write) and the web server (read). Typical Laravel-side translation:
// config/filesystems.php
'h5p' => ['driver' => 'local', 'root' => storage_path('h5p'), 'url' => env('APP_URL').'/h5p-assets'],
and a Route::get('/h5p-assets/{path}') ->where('path', '.*') controller that streams from that disk with long Cache-Control — or just a public/h5p-assets symlink. Do not use S3 as the primary root: H5P core does direct file_put_contents/scandir in places, so a local root plus a CDN in front is the low-friction path.
3. Content is two columns, not a blob. The model is content_id + parameters (JSON of the content's settings) + library (e.g. H5P.InteractiveVideo 1.24). Treat parameters as opaque; never hand-parse or mutate it. Versioned library strings are how H5P decides to run upgrade scripts on load — keep old library versions on disk after an upgrade or existing content breaks.
4. Build the H5PIntegration payload once, cache it. The player needs a JS settings object listing core scripts, content dependencies, contents and ajaxPath. Compute it per content type, cache it (Redis) keyed by library + content type version, and inject it into your Blade/iframe wrapper. Regenerating it per request is the main source of slow first paint.
5. Capture results through the AJAX contract. Learner answers, xAPI statements, and content-user-data (resume state) all stream through the ajax endpoint. Two integration tips:
h5p_content_user_data for resume/attempt history; it's the closest thing to a "gradebook" the bundle gives you out of the box. Map it to a Laravel h5p_results table if you're building reporting.6. Iframe is the safest embed. Rather than shipping core JS/CSS into your Laravel layout, render the H5P player in a same-origin iframe on the Symfony host. It isolates H5P's global H5PIntegration/H5P namespace, its CSS resets, and its jQuery assumptions from your app. Communicate via postMessage for xAPI/completed events.
7. Porting checklist if you go the Laravel route. Reimplement, in order: config + Flysystem root → migrations for the six core tables (utf8mb4, content_id unsigned bigint, long indexes on library/name) → the editor/player/AJAX controllers → asset route → a H5PEditorStorage/H5PEditorAjaxInterface bridge backed by Eloquent. Budget a week; H5P core's interfaces are global-state-heavy.
Vendor risk is real. 11 stars, 0 dependents, one maintainer, first stable-ish release Feb 2025. Pin the exact version ("jorisdugue/h5p-bundle": "1.x.y"), commit your composer.lock, and be prepared to fork. If you're doing the sidecar approach, treat the whole Symfony app as disposable — it should be reproducible from config.
Path/public-URL mismatch is the #1 failure mode. Symptom: iframe renders blank, console shows 404s for h5p-core/js/*.js or content .h5p dependencies. storage.path (filesystem) and the URL the web server serves must be the same directory. Check with curl -I on a known asset before debugging anything else.
Permissions. libraries/, content/, files/, cachedassets/ must be writable by the PHP user and readable by nginx/Apache. cachedassets/ in particular gets rewritten on library changes; on containerised deploys put it on a writable volume or accept cold-cache rebuilds.
.h5p uploads are zip files containing JS and PHP-checkable content. Always validate against H5P's own library validation, restrict upload to admins, and never serve the libraries/ directory with PHP execution enabled.
CSRF, sessions, and AJAX. If H5P editor saves fail with a generic "error" toast, look at /h5p/ajax responses in the Network tab — most often it's a firewall/CSRF rule rejecting the POST or a session user not being resolved. Loosen only that path.
Library upgrades don't migrate content backwards. Upgrading H5P.CoursePresentation leaves old content pinned to the old library. Keep previous versions in libraries/ and test a sample content item after every bump. There is no "downgrade" path.
Debugging ladder: (1) Network tab for 4xx/5xx on /h5p/ajax; (2) var/log/dev.log for the PHP exception behind it; (3) inspect the JSON in h5p_content.parameters — a malformed or empty parameters renders an empty player with no error; (4) var/h5p/cachedassets/ — clear it if CSS/JS look stale after a deploy.
Extension points. Prefer decorating the bundle's controllers/services over forking: swap the storage adapter via the service alias, add a Doctrine listener on the content entity for audit/versioning, or wrap the AJAX controller to log xAPI statements into your own analytics table. The bundle's entities are plain Doctrine — a postPersist listener is the cheapest place to push "content created" events into your Laravel app.
Don't fight H5P's globals. If you're porting, accept that H5PCore, H5PEditor, and H5PIntegration are not PSR/DI-friendly. Wrap them in one adapter class behind a Laravel contract; don't try to inject them everywhere or you'll be debugging bootstrap order forever.
How can I help you explore Laravel packages today?