rusticisoftware/tincan
PHP library for implementing the Experience API (Tin Can/xAPI). Provides tools to build and send statements, integrate with an LRS, and work with xAPI data. Install via Composer, includes PHPUnit tests and phpDocumentor docs generation.
Installation:
composer require rusticisoftware/tincan
Requires PHP 5.5+ (PHP 7+ recommended).
Basic Statement Creation:
use TinCan\Statement;
use TinCan\Agent;
use TinCan\Activity;
use TinCan\Verb;
use TinCan\RemoteLRS;
// Initialize RemoteLRS with your LRS endpoint and credentials
$lrs = new RemoteLRS('https://your-lrs-endpoint.com/xapi/statements/', '1.0.3', 'username', 'password');
// Create an Agent (learner)
$agent = new Agent('http://example.com/agents/123', 'http://example.com/agents/123#me', 'John Doe');
// Create an Activity (learning activity)
$activity = new Activity('http://example.com/activities/456', 'http://adlnet.gov/expapi/activities/course');
// Create a Verb (action)
$verb = new Verb('http://adlnet.gov/expapi/verbs/completed');
// Create and send a Statement
$statement = new Statement($agent, $verb, $activity);
$result = $lrs->sendStatement($statement);
First Use Case:
Track a user's interaction with a learning activity (e.g., completing a course module). Use the Statement class to encapsulate the event, then send it to your LRS.
tests/ directory for real-world usage patterns and edge cases.TinCan/Statement.php for core statement construction logic.TinCan/RemoteLRS.php for LRS communication details (authentication, headers, error handling).$statement = (new Statement($agent, $verb, $activity))
->withResult(new Result('completed', ['score': '1.0']))
->withContext(new Context(['extensions': ['http://example.com/extensions/foo' => 'bar']]));
$json = $statement->asVersion('1.0.3')->toJSON();
$attachment = new Attachment('file:///path/to/file.pdf', 'application/pdf');
$statement = $statement->withAttachment($attachment);
file:// or http:// URLs:
$attachment = new Attachment('http://example.com/file.pdf');
$statements = $lrs->queryStatements([
'agent' => 'http://example.com/agents/123',
'verb' => 'http://adlnet.gov/expapi/verbs/completed',
'since' => '2023-01-01T00:00:00Z',
'limit' => 10
]);
If-None-Match for conditional requests (ETag support):
$lrs->queryStatements([...], ['If-None-Match' => '*']);
RemoteLRS constructor.$lrs->sendStatement($statement, ['X-Custom-Header' => 'value']);
$lrs = new RemoteLRS($endpoint, $version, $username, $password, [
'proxy' => ['http' => 'http://proxy.example.com:8080']
]);
try {
$result = $lrs->sendStatement($statement);
} catch (TinCan\Exception\TinCanException $e) {
Log::error('LRS Error: ' . $e->getMessage());
}
Result objects for success:
if ($result->isSuccess()) {
// Handle success
}
AppServiceProvider:
public function register()
{
$this->app->singleton('tincan.lrs', function ($app) {
return new RemoteLRS(
config('tincan.lrs.endpoint'),
config('tincan.lrs.version'),
config('tincan.lrs.username'),
config('tincan.lrs.password')
);
});
}
php artisan vendor:publish --provider="YourServiceProvider"
public function handle($request, Closure $next)
{
$response = $next($request);
if ($request->user() && $request->is('learning/*')) {
$statement = (new Statement(
$request->user()->agent(),
new Verb('http://example.com/verbs/accessed'),
new Activity('http://example.com/activities/' . $request->path())
))->withContext(new Context([
'extensions' => ['page' => $request->path()]
]));
app('tincan.lrs')->sendStatement($statement);
}
return $response;
}
// app/Console/Commands/TestLRS.php
public function handle()
{
$lrs = app('tincan.lrs');
$statement = new Statement(
new Agent('http://example.com/agents/test'),
new Verb('http://adlnet.gov/expapi/verbs/test'),
new Activity('http://example.com/activities/test')
);
try {
$result = $lrs->sendStatement($statement);
$this->info('LRS Test: ' . ($result->isSuccess() ? 'Success' : 'Failed'));
} catch (Exception $e) {
$this->error('LRS Error: ' . $e->getMessage());
}
}
UserCompletedCourse):
public function handle(UserCompletedCourse $event)
{
$statement = (new Statement(
$event->user->agent(),
new Verb('http://adlnet.gov/expapi/verbs/completed'),
new Activity($event->course->activityId())
))->withResult(new Result('completed', ['score' => $event->score]));
app('tincan.lrs')->sendStatement($statement);
}
$cacheKey = 'xapi:statements:user:' . $userId;
$statements = Cache::remember($cacheKey, now()->addHours(1), function () use ($lrs, $userId) {
return $lrs->queryStatements([
'agent' => $userId,
'limit' => 100
]);
});
Immutable Objects:
$statement->withResult() instead of $statement->result = ...) will result in silent failures. Always use fluent methods.Version Mismatches:
1.0.1) may reject 1.0.3 statements. Explicitly set the version:
$statement->asVersion('1.0.1')->toJSON();
Attachment Handling:
Attachment must be absolute or use file:// protocol. Relative paths will fail:
// ❌ Fails
$attachment = new Attachment('file.pdf');
// ✅ Works
$attachment = new Attachment('file:///var/www/file.pdf');
ETag Issues:
If-None-Match: * may not work as expected with all LRS implementations. Test thoroughly.How can I help you explore Laravel packages today?