h5p/h5p-core
H5P Core is the PHP foundation for integrating interactive H5P content into Laravel and PHP apps. It manages content types, libraries, dependencies, and rendering so you can embed rich HTML5 activities and quizzes.
h5p/h5p-core is not a Laravel package — it is the framework-agnostic PHP engine behind H5P (the HTML5 authoring/rendering format used for interactive videos, quizzes, course presentations). It ships H5PCore, H5PEditor, H5PValidator, H5PStorage, H5PExport, H5PDevelopment and the client-side JS/CSS, but zero service providers, config, facades, migrations or Eloquent models. Day-to-day, you will spend most of your time writing the adapter that plugs it into Laravel.
Minimal setup
composer require h5p/h5p-core h5p/h5p-editor-php-library
Then, in order:
storage/app/h5p/{content,libraries,editor,temp,exports,cachedassets}.vendor/h5p/h5p-core/js, vendor/h5p/h5p-core/styles, and the editor's scripts/styles into public/vendor/h5p (or serve them via a route; don't try to Vite them).config/h5p.php with path (absolute, trailing slash), url (absolute, trailing slash), export, embed, dev_mode.H5PFrameworkInterface — this is the whole job. It is a large interface (content CRUD, library CRUD, file storage, translations, settings, whitelist). Bind it as a singleton, and either back it with Eloquent or copy the SQL schema used by the WordPress/Drupal modules. H5PDefaultStorage covers file I/O if you don't want to write it.H5PCore in a service provider:$this->app->singleton(H5PCore::class, fn ($app) => new H5PCore(
$app->make(H5PFrameworkInterface::class),
$app->getLocale(),
config('h5p.path').'/',
url('h5p').'/',
config('h5p.export'),
config('h5p.embed'),
));
Where to look first
H5PFrameworkInterface — the contract you must satisfy; read it end to end before writing anything.H5PValidator / H5PStorage::savePackage() — how a .h5p zip becomes an installed library or a content record.H5PEditorAjaxInterface (in h5p-editor-php-library) — the contract for the editor's AJAX endpoints.First use case: render an existing .h5p file. Upload it, run H5PValidator::isValidPackage() → H5PStorage::savePackage(), persist the content row, then build the H5PIntegration JS object and a div.h5p-content on a Blade view. Once that renders, you have proved paths, URLs, asset aggregation and the framework adapter all work.
1. One adapter class, bound as a singleton.
Subclass or implement H5PFrameworkInterface in app/H5P/LaravelFramework.php, inject ConnectionInterface/repositories, and bind it once. Keep it dumb — no business logic. All the stateful weirdness (library usage counters, dependency trees) lives in its methods.
2. Two distinct pipelines — libraries vs. content.
Library install (upload a content type / fetch from the H5P Hub) and content save (authoring) share H5PValidator but diverge in H5PStorage. Wrap each in its own action class:
$validator = new H5PValidator($framework, $core, new H5PDefaultStorage($tmpDir));
if (!$validator->isValidPackage(true, false)) {
throw new H5PValidationException($validator->getErrors());
}
$storage = new H5PStorage($framework, $core);
$storage->savePackage(null, $libraryIdForUpgrade);
3. Editor as a controller + AJAX router.
Serve a Blade view containing an empty <div id="h5p-editor"> and a tiny config script; the editor JS does the rest. Then expose one endpoint that dispatches on ?action= (libraries, content-type, translations, files, library-install, library-upload, get-content-type-cache). Keep this route behind your normal auth middleware — the editor happily lets anyone install libraries otherwise.
4. Build H5PIntegration yourself.
H5PCore won't emit it for you. Assemble the array (baseUrl, url, contents, core.scripts, core.styles, core.version, l10n, ajax.*, saveFreq, hubIsEnabled) and inline it as JSON. Cache it per content+locale; it is expensive to rebuild because getCachedAssets() aggregates every library's JS/CSS.
5. Aggregate assets, don't load per-library files.
Call H5PCore::getCachedAssets($missingLibraries), write the returned scripts/styles into cachedassets/, and reference those files with a hash-based filename. Regenerate on library install/upgrade and on H5P_VERSION bumps. The $missingLibraries out-param is your signal to surface "content type X is missing" instead of rendering a blank box.
6. Embeds: prefer iframe for anything untrusted.
H5PCore::determineEmbedType() gives you div / iframe / h5p. The div type runs H5P JS in your page context — fine for first-party authors, bad for user-uploaded content. The iframe route lets you serve a dedicated, CSP-locked page and add signed URLs.
7. Export and re-import.
H5PExport::createExport($contentId) writes a .h5p into exports/. Pair it with a queued job and a signed download route; exports of large Course Presentations are slow and disk-heavy.
8. State and xAPI.
H5P calls back for contentUserData (resume state) and xAPI statements. Implement these as authenticated POST endpoints keyed by contentId + dataType + subContentId, and decide early whether you persist xAPI or just accept-and-drop it (the client tolerates failures, but resume state breaks silently if you get this wrong).
9. Multi-tenancy.
The library assumes one global storage tree. For per-tenant content, construct H5PCore per-tenant with a different path/url rather than trying to scope one instance. Bind it with a scoped container binding.
10. Testing.
You can't meaningfully unit-test H5P output, but you can test your framework adapter against an in-memory SQLite schema and test the AJAX router's action dispatch. Use H5PDevelopment with a separate dev/ library folder to iterate on content types without reinstalling zips.
h5p-editor-php-library carry their own terms.H5PCore's $path must be an absolute directory with a trailing slash and $url a full URL with a trailing slash. Get it wrong and you get silent 404s on library files and a blank iframe with no console error from PHP..h5p files are ZIPs. getWhitelist() (extensions) and the library whitelist must be restrictive; reject anything with .. paths. Never trust a .h5p from a non-admin.h5p-php-library, h5p-editor-php-library, the H5PIntegration.core.version, and every installed content type must agree. Pin all three in composer.json/package.json, and treat "editor loads but the content type panel is empty" as a version mismatch until proven otherwise.temp/ grows forever. Failed validations and abandoned editor uploads leave orphaned directories. Schedule a nightly prune of temp/ older than N hours, and of cachedassets/ files not referenced by the current manifest.cachedassets/) in your deploy script — otherwise users get old H5P JS against new PHP and see "Unable to find constructor".alias to storage/app/h5p or a dedicated streaming controller with Accept-Ranges — a normal response()->file() on every media request will fall over under load.X-Frame-Options fight the editor. The editor lives in an iframe and loads h5p.org for the Hub. You need frame-src/frame-ancestors for your own origin and connect-src/script-src allowances; SAMEORIGIN on the editor route is fine, DENY breaks it entirely.H5PContentHub calls h5p.org to fetch the content-type catalogue. Behind a firewall this hangs until timeout — disable the Hub (hubIsEnabled = false) and ship a curated set of pre-installed libraries instead.H5PValidator::getErrors() and H5PEditor's error bag are the only diagnostics you get; they're easy to drop on the floor. Always log them, and surface the library machine names in the UI so support can act.H5PDevelopment left on in production silently serves from dev/ and makes installed content disappear. Gate it on app()->isLocal() explicitly.saveLibraryUsage()/deleteLibraryUsage() maintain a usage counter; if you delete content rows outside H5P's API, that counter drifts and deleteLibrary() will refuse to clean up. Delete content through H5PStorage, or write a reconciliation command.H5PFrameworkInterface (the main lever), H5PEditorStorage for where editor files land, and H5PEditorAjaxInterface for routing. Beyond those, the library expects you to subclass rather than configure.How can I help you explore Laravel packages today?