php-xapi/repository-doctrine
Doctrine ORM persistence backend for php-xapi, providing repository implementations for storing and retrieving xAPI statements, agents, activities, and related learning data in relational databases. Integrates Doctrine entities and mappings with the php-xapi repository interfaces.
php-xapi/repository-doctrine is the Doctrine ORM persistence layer for the php-xapi suite — it lets you store and query Experience API (xAPI) statements in a relational database. In Laravel you'll usually reach for it when you need to keep the statements your app emits or receives (learning records, analytics, LRS-style reporting) rather than just serialize them over HTTP.
Minimal steps:
composer require php-xapi/repository-doctrine:^0.3
It is not Eloquent and ships no Laravel service provider, so you need a Doctrine EntityManager. Either add laravel-doctrine/orm or build an EntityManager in a small provider. Then register the package's entity mapping (the entities live under XApi\Repository\Doctrine\Entity and use XML/attribute metadata) in your Doctrine config, and finally bind the repository interface from php-xapi/repository-api to the new StatementRepository in XApi\Repository\Doctrine\Repository\Mapping:
// AppServiceProvider::register()
$this->app->singleton(StatementRepository::class, function ($app) {
return new DoctrineStatementRepository($app->make(EntityManagerInterface::class));
});
Look first at: the mapping classes in XApi\Repository\Doctrine\Mapping (they translate model ⇄ entity, and v0.3.0 finally covers every statement property), the StatementRepository itself, and the php-xapi/repository-api query objects.
First use case: create the schema (orm:schema-tool:create or a generated migration), store a statement built with php-xapi/model, then read it back by ID and by query.
$repository->storeStatement($statement);
$query = new StatementQuery();
$query->setLimit(50); // plus verb / agent / activity / since / until filters
$result = $repository->getStatements($query);
foreach ($result->getStatements() as $s) { /* ... */ }
$result->getTotal(); // for pagination UI
Bind the interface, not the class. Inject XApi\Repository\Api\StatementRepository everywhere and let the container resolve the Doctrine implementation. The v0.3.0 rename (see Gotchas) is exactly the kind of thing that hurts if you type-hinted the concrete class across your app.
Statement ingestion. Treat the repository as a write-through sink for a queue job. Statements arrive as JSON, you deserialize with php-xapi/serializer into model objects, then:
$em->transactional(function ($em) use ($repository, $statements) {
$repository->storeStatements($statements); // batch, not one-by-one
});
Batching matters: each statement touches multiple tables (statement, actor, verb, activity, results, context, attachments), so per-statement calls are round-trip heavy.
Querying. Build StatementQuery objects from your HTTP/request layer rather than exposing Doctrine. The repository translates the query into SQL (joins + JSON conditions), so filtering happens in the database, not in PHP — good for large tables, but only for the fields the query API models. Anything beyond that (custom extensions, result scores) should be a separate reporting table or a raw DB query.
Two-layer architecture. Keep the API/model layer (php-xapi/model objects) and the persistence layer separate; never leak Doctrine entities out of your repository/service classes. The Mapping classes exist precisely so you can convert at the boundary.
Testing. With php-xapi/test-fixtures:^1.0 you get stable fixture statements — assert round-trip fidelity (store → getStatement($id) → deep-equal to the original). That single test catches almost every mapping regression from version bumps.
Laravel integration tips. Wrap schema creation in a real migration so deploy pipelines stay deterministic; use doctrine:migrations:diff if you have laravel-doctrine/migrations. Add a thin StatementRepository decorator for app-specific concerns (tenant scoping, soft deletes, event dispatch) instead of subclassing the Doctrine class.
v0.3.0 is a hard breaking change. MappedStatement and MappedVerb were removed from php-xapi/model, and MappedStatementRepository is gone. Any use XApi\Model\MappedStatement; or use ...\MappedStatementRepository; will fatal at autoload time. Grep for Mapped before upgrading and swap to XApi\Repository\Doctrine\Mapping\Statement / Verb and the new StatementRepository.
Upgrade the whole suite together. v0.3.0 bumps php-xapi/model and php-xapi/test-fixtures to ^1.0 and php-xapi/repository-api to ^0.3. Mixed versions of serializer, client or http-* packages will produce unresolvable conflicts. When Composer complains, run composer why-not php-xapi/model ^1.0 to find the lagging package rather than pinning php-xapi/model back.
Doctrine mapping isn't auto-discovered. Forgetting to register the package's mapping paths results in "class is not a valid entity" errors at runtime, not at boot. Register it explicitly and clear any metadata/query cache after deploying a mapping-affecting upgrade — stale second-level or query cache is a classic source of "my new property is always null".
Schema and column types. Statements are large; attachment binary content and long JSON bodies need LONGTEXT/jsonb, not default TEXT. On MySQL, use utf8mb4 and watch index length on identifier columns. SQLite works for tests but JSON functions differ — don't assume feature parity between your test DB and production.
Pagination cost. getTotal() implies a COUNT query over the same joins; on multi-million-row statement tables that's the slow part of your report page. Consider capping total computation or caching it per filter set.
Transaction boundaries. Store statements and their attachments in the same transaction. A partially written statement (row present, attachment missing) will deserialize into a subtly invalid model rather than erroring loudly.
Extension point. If you outgrow the implementation, the cleanest path is a decorator implementing the repository-api interface that delegates to the Doctrine repository — that keeps the mapper classes (which are individually unit-testable in XApi\Repository\Doctrine\Mapping) reusable even if you swap the storage engine.
How can I help you explore Laravel packages today?