laralocker

Laravel + Learning Locker || A Laravel Package for Learning Locker.

View the Project on GitHub ijeffro/laralocker

LaraLocker

Tests Coverage: 100%, enforced in CI PHPStan Code style
Latest version on Packagist Total downloads PHP ^8.2 Laravel 12 | 13 xAPI 1.0.3 MIT License

A Laravel API connector for Learning Locker®, the open-source Learning Record Store.

LaraLocker talks to Learning Locker’s two HTTP APIs from Laravel:

It is only a connector. It adds no routes, controllers, migrations or database tables to your app.

Requirements

Installation

composer require ijeffro/laralocker

In Learning Locker, open Settings → Clients, create a client, and give it the scopes you need: API All for the management API, and xAPI All (or xAPI read/write) for statements. Then add it to .env:

LEARNING_LOCKER_URL=https://your-learning-locker.example.com
LEARNING_LOCKER_KEY=your-client-key
LEARNING_LOCKER_SECRET=your-client-secret

LEARNING_LOCKER_URL is the root of the install, without /api or /data/xAPI.

To change the timeout, the xAPI language or the account home page, publish the config:

php artisan vendor:publish --tag=laralocker-config

Check the connection:

use LearningLocker;

LearningLocker::ping();        // true when Learning Locker answers
LearningLocker::clientInfo();  // the client these credentials belong to, with its scopes

Management API

Plural methods address the collection. Singular methods take a Learning Locker _id.

LearningLocker::stores()->get();                       // every store (LRS)
LearningLocker::store($id)->get();                     // one store
LearningLocker::store($id)->get(['_id', 'title']);     // only some fields
LearningLocker::store()->create(['title' => 'Main']);
LearningLocker::store($id)->update(['title' => 'Renamed']);
LearningLocker::store($id)->delete();

Filter, sort and page with MongoDB-style arguments:

LearningLocker::clients()
    ->where(['lrs_id' => $storeId])
    ->sort(['createdAt' => -1])
    ->skip(20)
    ->limit(10)
    ->get();

LearningLocker::personas()->where(['name' => 'Jane Doe'])->first();
LearningLocker::personas()->count();

For large collections, use Learning Locker’s cursor-based connection API:

$page = LearningLocker::personas()->paginate(first: 100);

while ($page['pageInfo']['hasNextPage']) {
    $page = LearningLocker::personas()->paginate(100, $page['pageInfo']['endCursor']);
}
Method Learning Locker model
organisations() / organisation($id) /api/v2/organisation
stores() / store($id) /api/v2/lrs
clients() / client($id) /api/v2/client
users() / user($id) /api/v2/user
roles() / role($id) /api/v2/role
queries() / query($id) /api/v2/query
dashboards() / dashboard($id) /api/v2/dashboard
visualisations() / visualisation($id) /api/v2/visualisation
exports() / export($id) /api/v2/export
downloads() / download($id) /api/v2/download
statementForwarding($id) /api/v2/statementforwarding
personas() / persona($id) /api/v2/persona
personaIdentifiers() / personaIdentifier($id) /api/v2/personaIdentifier
personaAttributes() / personaAttribute($id) /api/v2/personaattribute
personaImports() / personaImport($id) /api/v2/personasimport
statements() / statement($id) /api/v2/statement (read and delete only)
resource($model, $id) any other model, e.g. Enterprise-only journey

Run an aggregation over the organisation’s statements:

LearningLocker::aggregate([
    ['$match' => ['statement.verb.id' => 'http://adlnet.gov/expapi/verbs/completed']],
    ['$group' => ['_id' => '$statement.object.id', 'count' => ['$sum' => 1]]],
], ['maxTimeMS' => 5000]);

To use more than one Learning Locker client, pass other credentials:

LearningLocker::connect($url, $key, $secret)->stores()->get();

xAPI statements

Build and send a statement:

use xAPI;

$id = xAPI::actor(['name' => 'Jane Doe', 'email' => 'jane@example.com'])
    ->did('completed')
    ->what('https://example.com/courses/intro', 'Introduction', 'http://adlnet.gov/expapi/activities/course')
    ->scored(['score' => ['scaled' => 0.9], 'success' => true, 'completion' => true])
    ->context(['platform' => 'My app'])
    ->send();

Send statements you have already built, one at a time or in a batch:

LearningLocker::statements()->send([$statement, $anotherStatement]);

Read statements back:

xAPI::statements(['agent' => ['email' => 'jane@example.com'], 'limit' => 10]);
xAPI::statement($statementId);
xAPI::more($page['more']);

Errors

Any 4xx or 5xx from Learning Locker throws Ijeffro\Laralocker\Exceptions\LearningLockerException. $e->status() gives the HTTP status, and $e->response holds the full response.

try {
    LearningLocker::store($id)->get();
} catch (LearningLockerException $e) {
    if ($e->status() === 404) {
        // no such store
    }
}

A 401 means the key or secret is wrong, or the client has been disabled. A 403 means the client lacks the scope for that call.

Testing

composer test            # PHPUnit
composer test-coverage   # PHPUnit with a coverage report (needs pcov or Xdebug)
composer lint            # Pint
composer analyse         # PHPStan, level 8

On every push and pull request, CI runs the following:

The tests fake Learning Locker with Http::fake(), so they need no credentials. You can fake it the same way in your own app:

Http::fake(['your-learning-locker.example.com/api/v2/lrs*' => Http::response([['_id' => '1']])]);

Upgrading from 1.x or 2.x

Version 3 is a rewrite. It keeps the facade calls but drops everything that wasn’t the API client. See the CHANGELOG and the Upgrade guide.

More

Please report security issues privately through GitHub security advisories, not the issue tracker.

Credits

License

MIT. See the license file.

Learning Locker® is a registered trademark of Learning Pool. This package is not affiliated with Learning Pool.