ILIAS  release_8 Revision v8.19
All Data Structures Namespaces Files Functions Variables Modules Pages
class.ilXapiStatementEvaluation.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
31 {
36  protected array $resultStatusByXapiVerbMap = [
37  ilCmiXapiVerbList::COMPLETED => "completed",
38  ilCmiXapiVerbList::PASSED => "passed",
39  ilCmiXapiVerbList::FAILED => "failed",
41  ];
42 
43  protected array $resultProgressByXapiVerbMap = [
44  ilCmiXapiVerbList::PROGRESSED => "progressed",
45  ilCmiXapiVerbList::EXPERIENCED => "experienced"
46  ];
47 
48  protected ilObject $object;
49 
50  //todo
51  protected ilLogger $log;
52 
53  protected ?int $lpMode;
54 
58  public function __construct(ilLogger $log, ilObject $object)
59  {
60  $this->log = $log;
61  $this->object = $object;
62 
63  $objLP = ilObjectLP::getInstance($this->object->getId());
64  $this->lpMode = $objLP->getCurrentMode();
65  }
66 
67  public function evaluateReport(ilCmiXapiStatementsReport $report): void
68  {
69  foreach ($report->getStatements() as $xapiStatement) {
70  #$this->log->debug(
71  # "handle statement:\n".json_encode($xapiStatement, JSON_PRETTY_PRINT)
72  #);
73 
74  // ensure json decoded non assoc
75  $xapiStatement = json_decode(json_encode($xapiStatement));
76  $cmixUser = $this->getCmixUser($xapiStatement);
77 
78  $this->evaluateStatement($xapiStatement, $cmixUser->getUsrId());
79 
80  $this->log->debug('update lp for object (' . $this->object->getId() . ')');
81  if ($cmixUser->getUsrId() != ANONYMOUS_USER_ID) {
82  ilLPStatusWrapper::_updateStatus($this->object->getId(), $cmixUser->getUsrId());
83  }
84  }
85  }
86 
87  public function getCmixUser(object $xapiStatement): \ilCmiXapiUser
88  {
89  $cmixUser = null;
90  if ($this->object->getContentType() == ilObjCmiXapi::CONT_TYPE_CMI5) {
92  $this->object->getId(),
93  $xapiStatement->actor->account->name
94  );
95  } else {
97  $this->object->getId(),
98  str_replace('mailto:', '', $xapiStatement->actor->mbox)
99  );
100  }
101  return $cmixUser;
102  }
103 
104  public function evaluateStatement(object $xapiStatement, int $usrId): void
105  {
106  global $DIC;
107  $xapiVerb = $this->getXapiVerb($xapiStatement);
108 
109  if ($this->isValidXapiStatement($xapiStatement)) {
110  // result status and if exists scaled score
111  if ($this->hasResultStatusRelevantXapiVerb($xapiVerb)) {
112  if (!$this->isValidObject($xapiStatement)) {
113  return;
114  }
115  $userResult = $this->getUserResult($usrId);
116 
117  $oldResultStatus = $userResult->getStatus();
118  $newResultStatus = $this->getResultStatusForXapiVerb($xapiVerb);
119 
120  // this is for both xapi and cmi5
121  if ($this->isResultStatusToBeReplaced($oldResultStatus, $newResultStatus)) {
122  $this->log->debug("isResultStatusToBeReplaced: true");
123  $userResult->setStatus($newResultStatus);
124  }
125 
126  if ($this->hasXapiScore($xapiStatement)) {
127  $xapiScore = $this->getXapiScore($xapiStatement);
128  $this->log->debug("Score: " . $xapiScore);
129  $userResult->setScore((float) $xapiScore);
130  }
131  $userResult->save();
132 
133  // only cmi5
134  if ($this->object->getContentType() == ilObjCmiXapi::CONT_TYPE_CMI5) {
135  if (($xapiVerb == ilCmiXapiVerbList::COMPLETED || $xapiVerb == ilCmiXapiVerbList::PASSED) && $this->isLpModeInterestedInResultStatus($newResultStatus, false)) {
136  // it is possible to check against authToken usrId!
137  $cmixUser = $this->getCmixUser($xapiStatement);
138  // avoid multiple satisfied
139  if (!$cmixUser->getSatisfied()) {
140  $cmixUser->setSatisfied(true);
141  $cmixUser->save();
142  $this->sendSatisfiedStatement($cmixUser);
143  }
144  }
145  }
146  }
147  // result progress (i think only cmi5 relevant)
148  if ($this->hasResultProgressRelevantXapiVerb($xapiVerb)) {
149  $userResult = $this->getUserResult($usrId);
150  $progressedScore = $this->getProgressedScore($xapiStatement);
151  if ($progressedScore !== null && $progressedScore > 0) {
152  $userResult->setScore((float) ($progressedScore / 100));
153  $userResult->save();
154  }
155  }
156  }
157  }
158 
159  protected function isValidXapiStatement(object $xapiStatement): bool
160  {
161  if (!isset($xapiStatement->actor)) {
162  return false;
163  }
164 
165  if (!isset($xapiStatement->verb) || !isset($xapiStatement->verb->id)) {
166  return false;
167  }
168 
169  if (!isset($xapiStatement->object) || !isset($xapiStatement->object->id)) {
170  return false;
171  }
172 
173  return true;
174  }
175 
179  protected function isValidObject(object $xapiStatement): bool
180  {
181  if ($xapiStatement->object->id != $this->object->getActivityId()) {
182  $this->log->debug($xapiStatement->object->id . " != " . $this->object->getActivityId());
183  return false;
184  }
185  return true;
186  }
187 
188 
189  protected function getXapiVerb(object $xapiStatement): string
190  {
191  return $xapiStatement->verb->id;
192  }
193 
194  protected function getResultStatusForXapiVerb(string $xapiVerb): string
195  {
196  return $this->resultStatusByXapiVerbMap[$xapiVerb];
197  }
198 
199  protected function hasResultStatusRelevantXapiVerb(string $xapiVerb): bool
200  {
201  return isset($this->resultStatusByXapiVerbMap[$xapiVerb]);
202  }
203 
204  protected function getResultProgressForXapiVerb(string $xapiVerb)
205  {
206  return $this->resultProgressByXapiVerbMap[$xapiVerb];
207  }
208 
209  protected function hasResultProgressRelevantXapiVerb(string $xapiVerb): bool
210  {
211  return isset($this->resultProgressByXapiVerbMap[$xapiVerb]);
212  }
213 
214  protected function hasXapiScore(object $xapiStatement): bool
215  {
216  if (!isset($xapiStatement->result)) {
217  return false;
218  }
219 
220  if (!isset($xapiStatement->result->score)) {
221  return false;
222  }
223 
224  if (!isset($xapiStatement->result->score->scaled)) {
225  return false;
226  }
227 
228  return true;
229  }
230 
231  protected function getXapiScore(object $xapiStatement)
232  {
233  return $xapiStatement->result->score->scaled;
234  }
235 
236  protected function getProgressedScore(object $xapiStatement): ?float
237  {
238  if (!isset($xapiStatement->result)) {
239  return null;
240  }
241 
242  if (!isset($xapiStatement->result->extensions)) {
243  return null;
244  }
245 
246  if (!isset($xapiStatement->result->extensions->{'https://w3id.org/xapi/cmi5/result/extensions/progress'})) {
247  return null;
248  }
249  return (float) $xapiStatement->result->extensions->{'https://w3id.org/xapi/cmi5/result/extensions/progress'};
250  }
251 
252  protected function getUserResult(int $usrId): \ilCmiXapiResult
253  {
254  try {
255  $result = ilCmiXapiResult::getInstanceByObjIdAndUsrId($this->object->getId(), $usrId);
256  } catch (ilCmiXapiException $e) {
258  $result->setObjId($this->object->getId());
259  $result->setUsrId($usrId);
260  }
261 
262  return $result;
263  }
264 
265  protected function isResultStatusToBeReplaced(string $oldResultStatus, string $newResultStatus): bool
266  {
267  if (!$this->isLpModeInterestedInResultStatus($newResultStatus)) {
268  $this->log->debug("isLpModeInterestedInResultStatus: false");
269  return false;
270  }
271 
272  if (!$this->doesNewResultStatusDominateOldOne($oldResultStatus, $newResultStatus)) {
273  $this->log->debug("doesNewResultStatusDominateOldOne: false");
274  return false;
275  }
276 
277  if ($this->needsAvoidFailedEvaluation($oldResultStatus, $newResultStatus)) {
278  $this->log->debug("needsAvoidFailedEvaluation: false");
279  return false;
280  }
281 
282  return true;
283  }
284 
285  protected function isLpModeInterestedInResultStatus(string $resultStatus, ?bool $deactivated = true): ?bool
286  {
287  if ($this->lpMode == ilLPObjSettings::LP_MODE_DEACTIVATED) {
288  return $deactivated;
289  }
290 
291  switch ($resultStatus) {
292  case 'failed':
293 
294  return in_array($this->lpMode, [
298  ]);
299 
300  case 'passed':
301 
302  return in_array($this->lpMode, [
307  ]);
308 
309  case 'completed':
310 
311  return in_array($this->lpMode, [
316  ]);
317  }
318 
319  return false;
320  }
321 
322  protected function doesNewResultStatusDominateOldOne(string $oldResultStatus, string $newResultStatus): bool
323  {
324  if ($oldResultStatus == '') {
325  return true;
326  }
327 
328  if (in_array($newResultStatus, ['passed', 'failed'])) {
329  return true;
330  }
331 
332  if (!in_array($oldResultStatus, ['passed', 'failed'])) {
333  return true;
334  }
335 
336  return false;
337  }
338 
339  protected function needsAvoidFailedEvaluation(string $oldResultStatus, string $newResultStatus): bool
340  {
341  if (!$this->object->isKeepLpStatusEnabled()) {
342  return false;
343  }
344 
345  if ($newResultStatus != 'failed') {
346  return false;
347  }
348 
349  return $oldResultStatus == 'completed' || $oldResultStatus == 'passed';
350  }
351 
352  protected function sendSatisfiedStatement(ilCmiXapiUser $cmixUser): void
353  {
354  global $DIC;
355 
356  $lrsType = $this->object->getLrsType();
357  $defaultLrs = $lrsType->getLrsEndpoint();
358  //$fallbackLrs = $lrsType->getLrsFallbackEndpoint();
359  $defaultBasicAuth = $lrsType->getBasicAuth();
360  //$fallbackBasicAuth = $lrsType->getFallbackBasicAuth();
361  $defaultHeaders = [
362  'X-Experience-API-Version' => '1.0.3',
363  'Authorization' => $defaultBasicAuth,
364  'Content-Type' => 'application/json;charset=utf-8',
365  'Cache-Control' => 'no-cache, no-store, must-revalidate'
366  ];
367  /*
368  $fallbackHeaders = [
369  'X-Experience-API-Version' => '1.0.3',
370  'Authorization' => $fallbackBasicAuth,
371  'Content-Type' => 'application/json;charset=utf-8',
372  'Cache-Control' => 'no-cache, no-store, must-revalidate'
373  ];
374  */
375  $satisfiedStatement = $this->object->getSatisfiedStatement($cmixUser);
376  $satisfiedStatementParams = [];
377  $satisfiedStatementParams['statementId'] = $satisfiedStatement['id'];
378  $defaultStatementsUrl = $defaultLrs . "/statements";
379  $defaultSatisfiedStatementUrl = $defaultStatementsUrl . '?' . ilCmiXapiAbstractRequest::buildQuery($satisfiedStatementParams);
380 
381  $client = new GuzzleHttp\Client();
382  $req_opts = array(
383  GuzzleHttp\RequestOptions::VERIFY => true,
384  GuzzleHttp\RequestOptions::CONNECT_TIMEOUT => 10,
385  GuzzleHttp\RequestOptions::HTTP_ERRORS => false
386  );
387 
388  $defaultSatisfiedStatementRequest = new GuzzleHttp\Psr7\Request(
389  'PUT',
390  $defaultSatisfiedStatementUrl,
391  $defaultHeaders,
392  json_encode($satisfiedStatement)
393  );
394  $promises = array();
395  $promises['defaultSatisfiedStatement'] = $client->sendAsync($defaultSatisfiedStatementRequest, $req_opts);
396  try {
397  $responses = GuzzleHttp\Promise\Utils::settle($promises)->wait();
398  $body = '';
399  ilCmiXapiAbstractRequest::checkResponse($responses['defaultSatisfiedStatement'], $body, [204]);
400  } catch (Exception $e) {
401  $this->log->error('error:' . $e->getMessage());
402  }
403  }
404 }
isResultStatusToBeReplaced(string $oldResultStatus, string $newResultStatus)
const ANONYMOUS_USER_ID
Definition: constants.php:27
isLpModeInterestedInResultStatus(string $resultStatus, ?bool $deactivated=true)
doesNewResultStatusDominateOldOne(string $oldResultStatus, string $newResultStatus)
evaluateStatement(object $xapiStatement, int $usrId)
sendSatisfiedStatement(ilCmiXapiUser $cmixUser)
__construct(ilLogger $log, ilObject $object)
ilXapiStatementEvaluation constructor.
static getInstanceByObjIdAndUsrId(int $objId, int $usrId)
array $resultStatusByXapiVerbMap
http://adlnet.gov/expapi/verbs/satisfied: should never be sent by AU https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md#939-satisfied
needsAvoidFailedEvaluation(string $oldResultStatus, string $newResultStatus)
global $DIC
Definition: feed.php:28
$client
static getInstanceByObjectIdAndUsrIdent(int $objId, string $usrIdent)
evaluateReport(ilCmiXapiStatementsReport $report)
const LP_MODE_CMIX_COMPL_OR_PASSED_WITH_FAILED
static buildQuery(array $params, $encoding=PHP_QUERY_RFC3986)
static getInstance(int $obj_id)
static checkResponse(array $response, &$body, array $allowedStatus=[200, 204])
static _updateStatus(int $a_obj_id, int $a_usr_id, ?object $a_obj=null, bool $a_percentage=false, bool $a_force_raise=false)