No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DOMSCORM2004.php 38KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961
  1. <?php
  2. namespace Logipro\Bundle\SCORMBundle\LearningModels;
  3. use PHPUnit\Runner\Exception;
  4. class DOMSCORM2004 extends DOMSCORM
  5. {
  6. /**
  7. * *
  8. * @param string $XMLManifest
  9. */
  10. public function __construct(string $XMLManifest)
  11. {
  12. parent::__construct($XMLManifest);
  13. }
  14. /**
  15. * durée estimé par l'auteur du cours
  16. * @return array
  17. * * [datetime] => hh:ii:ss (ex. 01:45:00)
  18. * * [description] => Texte ecrit par l'auteur du module (ex.: Durée estimée de la formation)
  19. */
  20. public function getTypicalLearningTime()
  21. {
  22. $tlt = $this->manifest->getElementsByTagName("typicalLearningTime");
  23. $tltNode = $tlt->item(0);
  24. $children = $tltNode->childNodes;
  25. $result = array();
  26. foreach ($children as $node) {
  27. switch ($node->nodeName) {
  28. case 'datetime':
  29. case 'description': // IMPORTANT TODO: ce champ peut etre multilangue dans le manifest
  30. $result[$node->nodeName] = $node->nodeValue;
  31. break;
  32. }
  33. }
  34. return $result;
  35. }
  36. /**
  37. * table des matières d'une organization
  38. *
  39. * @param string $organizationId identifiant de l'organization
  40. * @return array
  41. */
  42. public function getTableOfContents(string $organizationId = null)
  43. {
  44. $result = array();
  45. $tlt = $this->manifest->getElementsByTagName("organizations");
  46. $organizations = $tlt->item(0);
  47. if ($organizationId == null) { // alors ce sera l'organization par defaut designée par le manifest
  48. $default = $organizations->attributes->getNamedItem("default")->nodeValue;
  49. } else {
  50. $default = $organizationId;
  51. }
  52. foreach ($organizations->childNodes as $organization) {
  53. if ($organization->nodeName == 'organization') { // on ne veut pas d'autre chose qu'une organization
  54. $identifier = $organization->attributes->getNamedItem("identifier")->nodeValue;
  55. if (($default == $identifier)||('mm-'.$default == $identifier)) { // ici expliquer pourquoi il faut ajouter 'mm-'
  56. // on parcours les items de l'organization
  57. $result = $this->organization2array($organization->childNodes);
  58. }
  59. }
  60. }
  61. return $result;
  62. }
  63. /**
  64. * construit le tableau arborescence de l'organization.
  65. * @param DOMNodeList $itemList
  66. * @return array
  67. * * [itemId]
  68. * * [itemId][title]
  69. * * [itemId][isvisible]
  70. * * [itemId][ressourceRef]
  71. * * [itemId][itemId] array()
  72. */
  73. private function organization2array(\DOMNodeList $itemList, array $organization = null)
  74. {
  75. if ($organization == null) {
  76. $organization = array();
  77. }
  78. foreach ($itemList as $itemNode) {
  79. if ($itemNode->nodeName == 'title') {
  80. $organization['title']=$itemNode->nodeValue;
  81. }
  82. if ($itemNode->nodeName == 'adlcp:prerequisites') {
  83. $organization['adlcp:prerequisites']=$itemNode->nodeValue;
  84. }
  85. if ($itemNode->nodeName == 'item') { // et voila la partie récursive
  86. $itemId = $itemNode->attributes->getNamedItem("identifier")->nodeValue;
  87. $organization[$itemId] = array();
  88. foreach ($itemNode->attributes as $node) {
  89. switch ($node->nodeName) {
  90. case 'identifierref':
  91. case 'isvisible':
  92. default:
  93. $organization[$itemId][$node->nodeName] = $node->nodeValue;
  94. break;
  95. }
  96. }
  97. $organization[$itemId] = $this->organization2array($itemNode->childNodes, $organization[$itemId]);
  98. }
  99. }
  100. return $organization;
  101. }
  102. public function getPreviousAndNext(string $organizationIdentifier, string $itemIdentifier)
  103. {
  104. }
  105. public function getRessource()
  106. {
  107. }
  108. /**
  109. * organization par défaut dans un paquet
  110. *
  111. * @return string identifiant de l'organization par default
  112. */
  113. public function getDefaultOrganization() : string
  114. {
  115. $tlt = $this->manifest->getElementsByTagName("organizations");
  116. $organizations = $tlt->item(0);
  117. $default = $organizations->attributes->getNamedItem("default")->nodeValue;
  118. return $default;
  119. }
  120. /**
  121. * retrouve l'ancetre commun de 2 items. La racine absolu des items sera l'identifiant de l'organization
  122. *
  123. * @param string $item1 1er item descendant commun
  124. * @param string $item2 2eme item descendant commun
  125. * @return string ancetre commun ou null si pas trouvé
  126. */
  127. public function findCommonAncestor(string $item1, string $item2) : ?string
  128. {
  129. // algo : on remonte un noeud-item jusqu'à la racine pour former une branche, puis on fait pareil pour le second
  130. // et on stoppe lorsque le noeud boservé est egal à un noeud de la branche.
  131. // si pas de resultat ca veut dire qu'on n'observe des arbres-organization differente ou qu'il y a un pb dans la structure
  132. // de l'arbre (mauvais nom de noeud)
  133. $branche = array();
  134. //creation de la branche pour l'item1
  135. $currentNode = $this->getItemByIdentifier($item1);
  136. do {
  137. $current = $currentNode->attributes->getNamedItem("identifier")->nodeValue;
  138. $branche[$current] = true;
  139. $currentNode = $currentNode->parentNode;
  140. } while (($currentNode->nodeName == 'item')||($currentNode->nodeName == 'organization'));
  141. //remontee de la branche de l'item2 jusqu'à trouver
  142. $currentNode = $this->getItemByIdentifier($item2);
  143. $result = null; // initialisation du resultat à la valeur pas trouvée
  144. do {
  145. $current = $currentNode->attributes->getNamedItem("identifier")->nodeValue;
  146. if (isset($branche[$current])) {
  147. $result = $current;
  148. }
  149. $branche[$current] = 1;
  150. $currentNode = $currentNode->parentNode;
  151. } while (($result == null)&&(($currentNode->nodeName == 'item')||($currentNode->nodeName == 'organization')));
  152. return $result;
  153. }
  154. /**
  155. * chemin sous forme de tableau aqssociation (dont la clef est l'identifiant de l'item/organization
  156. * dont le 1er element est l'item de depart et le dernier est l'item d'arrivé
  157. *
  158. * @param string $itemStart
  159. * @param string $itemEnd
  160. * @param boolean $withItemStart
  161. * @param boolean $withItemEnd
  162. * @return array|null liste des items constituant le path (tableau dont la clef est un identifiant d'item/organization)
  163. */
  164. public function getDirectPath(
  165. string $itemStart,
  166. string $itemEnd,
  167. bool $withItemStart = true,
  168. bool $withItemEnd = true
  169. ) : ?array {
  170. // algo: on remonte les 2 branches jusqu'a la racine ou jusqu'a trouvé l'autre
  171. $findEnd = false; // indicateur du fait que l'on est trouvé la fin
  172. $branche = array();
  173. $currentNode = $this->manifest->getElementById($itemStart);
  174. do {
  175. $current = $currentNode->attributes->getNamedItem("identifier")->nodeValue;
  176. if ((($withItemStart ==true) || ($current != $itemStart))&&(($withItemEnd ==true) || ($current != $itemEnd))) {
  177. $branche[$current] = true;
  178. }
  179. if ($current == $itemEnd) {
  180. $findEnd = true;
  181. }
  182. $currentNode = $currentNode->parentNode;
  183. } while (($findEnd == false)&&($currentNode->nodeName == 'item')||($currentNode->nodeName == 'organization'));
  184. if ($findEnd == true) {
  185. return $branche; // le probleme est resolu...
  186. }
  187. //sinon parcours avec demarrage par l'autre noeud
  188. $findStart = false; // indicateur du fait que l'on est trouvé la fin
  189. $branche = array();
  190. $currentNode = $this->manifest->getElementById($itemEnd);
  191. do {
  192. $current = $currentNode->attributes->getNamedItem("identifier")->nodeValue;
  193. if ((($withItemStart ==true) || ($current != $itemStart))&&(($withItemEnd ==true) || ($current != $itemEnd))) {
  194. $branche[$current] = true;
  195. }
  196. if ($current == $itemStart) {
  197. $findStart = true;
  198. }
  199. $currentNode = $currentNode->parentNode;
  200. } while (($findStart == false)&&($currentNode->nodeName == 'item')||($currentNode->nodeName == 'organization'));
  201. if ($findStart == false) {
  202. return null; // aucun resultat
  203. }
  204. $branche = array_reverse($branche);
  205. return $branche; // pas de resultat
  206. }
  207. /**
  208. * renvoi l'item/organization parent
  209. *
  210. * @param string $item
  211. * @return string|null item parent (null si c'est le parent de l'organization qui est demandé)
  212. */
  213. public function getParent(string $item) : ?string
  214. {
  215. $itemNode = $this->manifest->getElementById($item);
  216. $parent = $itemNode->parentNode;
  217. if (($parent == null)||($itemNode->nodeName =='organization')) {
  218. return null;
  219. }
  220. return $parent->attributes->getNamedItem("identifier")->nodeValue;
  221. }
  222. /**
  223. * renvoi un tableau associatif dont les clefs sont les items
  224. * et qui correspond à un applatissage de l'arborescence
  225. *
  226. * @param string $item item/organization racine de l'arbre (ou sous arbre)
  227. * @return array itemroot =>0, itemfirstchild => 1, itemfirstchildofchild => 2, ...
  228. */
  229. public function getFlatTree(string $item) : array
  230. {
  231. return $this->getFlatTreeRecursive($item); // cela pour bloquer toutes tentative de bricoler le 2eme parametre
  232. }
  233. private function getFlatTreeRecursive(string $item, array &$output = null) : array
  234. {
  235. $itemNode = $this->manifest->getElementById($item);
  236. $current = $itemNode->attributes->getNamedItem("identifier")->nodeValue;
  237. if ($output == null) {
  238. $output=array();
  239. }
  240. $output[$current] = sizeof($output);
  241. $enfantsNodeList = $itemNode->childNodes;
  242. for ($i=0; $i<$enfantsNodeList->count(); $i++) {
  243. $enfant = $enfantsNodeList->item($i);
  244. if ($enfant->nodeName == 'item') { // on fait bien attentino de ne traiter que les items...
  245. $enfantIdentifier = $enfant->attributes->getNamedItem("identifier")->nodeValue;
  246. $output = $this->getFlatTreeRecursive($enfantIdentifier, $output);
  247. }
  248. }
  249. return $output;
  250. }
  251. /**
  252. * tableau associatif des items enfant de l'item/organization passé en entrée
  253. * Les items sont stockés sous la forme de leur identifiant.
  254. *
  255. * @param string $item
  256. * @return array tableau des items enfants
  257. */
  258. public function getChildren(string $item) : array
  259. {
  260. $children = array();
  261. $itemNode = $this->manifest->getElementById($item);
  262. $childNodeList = $itemNode->childNodes;
  263. for ($i=0; $i<$childNodeList->length; $i++) {
  264. $childNode = $childNodeList->item($i);
  265. if ($childNode->nodeName == "item") {
  266. $child = $childNode->attributes->getNamedItem("identifier")->nodeValue;
  267. $children[] = $child;
  268. }
  269. }
  270. return $children;
  271. }
  272. /**
  273. * donne la valeur de l'attribut d'un tag à partir de son xpath
  274. * si le tag existe et si l'attribut n'existe pas c'est la valeur par défaut qui est retourné (fonctionnement
  275. * classique de xpath->query car schemaValidateSource a été lancée)
  276. * si l'attribut n'a pas de valeur par défaut : null
  277. * si le tag n'existe pas : null
  278. * attention les query sont complexifié car le namespace par défaut a été défini ( => pas possible de faire
  279. * query(//item) pour tout les tags du namespace de base; faire query(//*name="item))
  280. * Exemples :
  281. * ( '//*[name()="item" or name()="organization"][@identifier="T-01b"]/imsss:sequencing/imsss:controlMode' ,'flow' )
  282. * @param string $item Par exemples (valeurs/defaut):
  283. * * imsss:deliveryControls.tracked
  284. * @param string $path chemin de l'attribut
  285. * @return string valeur de l'attribut sous format chaine de caracteres => un transpitage sera necessaire
  286. * pour les attributs numérique
  287. */
  288. protected function getAttributeByPath(string $path, string $attribute) : ?string
  289. {
  290. $value = null; // valeur de l'attribut
  291. $nodeList = $this->xpath->query($path);
  292. if ($nodeList->length == 1) {
  293. $node = $nodeList->item(0);
  294. $node = $node->attributes->getNamedItem($attribute);
  295. if ($node !=null) {
  296. $value = $node->nodeValue;
  297. }
  298. }
  299. return $value;
  300. }
  301. /**
  302. * renvoie la valeur d'un attribut d'un tag descendant d'une sequencing
  303. * si le tag n'est pas présent effectue quand meme une recherche
  304. * si le tag est présent renvoie la valeur de l'attribut ou sa valeur par défaut
  305. *
  306. * scrute l'attribut dans le tag sequencingCollection/sequencing référencé par item/sequencing.IDRef s'il existe.
  307. * c'est ce tag qui est renvoyé en priorité.
  308. *
  309. * Exemples de couples chemin attribut :
  310. * * 'imsss:objectives/imsss:primaryObjective', 'satisfiedByMeasure'
  311. * * 'imsss:rollupRules/imsss:rollupRule/imsss:rollupConditions/imsss:rollupCondition', 'condition'
  312. *
  313. * @param string $tagPath chemin du tag
  314. * @param string $attributeName nom de l'attribut
  315. * @param string $item pour lequel on examine le sequencing
  316. * @return string|null meme une valeur numérique sera une chaine (Ex. "true" (et pas (bool)1 ))
  317. */
  318. public function getSequencingAttribute(string $tagPath, string $attributeName, string $item = null) : ?string
  319. {
  320. $value = null;
  321. if ($item != null) {
  322. // on tente de de récupérer la valeur dans sequencingCollection
  323. $itemNode = $this->getItemByIdentifier($item);
  324. $listItemNodes = $this->xpath->query('imsss:sequencing', $itemNode);
  325. if ($listItemNodes->length > 0) {
  326. $sequencingNode = $listItemNodes->item(0);
  327. $attributeNode = $sequencingNode->getAttributeNode('IDRef');
  328. if ($attributeNode != null) {
  329. $value = $this->getSequencingAttribute(
  330. 'imsss:sequencing[@ID="'.$attributeNode->nodeValue.'"]/'.$tagPath,
  331. $attributeName
  332. );
  333. if ($value != null) { //on tient la valeur
  334. return $value;
  335. }
  336. }
  337. }
  338. if ($value == null) { // dernière tentative pour renvoyer une valeur par défaut
  339. $value = SCORMTools::getAttributeDefaultValue($attributeName);
  340. }
  341. // sinon on récupère dans la sequence de l'item
  342. $query = '//*[name()="item" or name()="organization"][@identifier="'.$item.'"]/imsss:sequencing/'.$tagPath;
  343. } else {
  344. $query = '//imsss:sequencingCollection/'.$tagPath;
  345. $nodeList = $this->xpath->query($query);
  346. }
  347. $nodeList = $this->xpath->query($query);
  348. if ($nodeList->length == 1) {
  349. $node = $nodeList->item(0);
  350. $attributeNode = $node->attributes->getNamedItem($attributeName);
  351. if ($attributeNode !=null) {
  352. $value = $attributeNode->nodeValue;
  353. }
  354. }
  355. return $value;
  356. }
  357. /**
  358. * renvoie les regles sous forme d'un tableau
  359. * Exemple :
  360. * 0 => [
  361. * 'childActivitySet' => 'all',
  362. * 'minimumCount' => '0',
  363. * 'minimumPercent' => '0',
  364. * 'action' => 'satisfied',
  365. * 'conditionCombination' => 'any',
  366. * 'conditions' => [
  367. * 0 => [
  368. * 'operator' => 'noOp',
  369. * 'condition' => 'completed'
  370. * ]
  371. * ]
  372. * ] ]
  373. * @param string $item
  374. * @return array
  375. */
  376. public function getRollupRules(string $item) : array
  377. {
  378. $rules = array();
  379. // on commence par recuperer (si elles existent) les valeurs sur le sequencing collection
  380. $refSeqCol = $this->getAttributeByName($item, 'IDRef');
  381. if ($refSeqCol != null) {
  382. $query = 'imsss:sequencingCollection/imsss:sequencing[@ID="'.$refSeqCol.'"]/imsss:rollupRules/descendant::*';
  383. $listNodes = $this->xpath->query($query);
  384. $this->rollupNodesToArray($listNodes, $rules);
  385. }
  386. // recuperation des rules de la sequence de l'item
  387. $query = '//*[name()="item" or name()="organization"][@identifier="'.
  388. $item.'"]/imsss:sequencing/imsss:rollupRules/descendant::*';
  389. $listNodes = $this->xpath->query($query);
  390. $this->rollupNodesToArray($listNodes, $rules);
  391. return $rules;
  392. }
  393. private function rollupNodesToArray(\DOMNodeList $listNodes, array &$rules) : void
  394. {
  395. $indice = -1; // indices du plus haut niveau du tableau de rule...
  396. // dans cette boucle on joue sur le fait que les tag sont ordonnés hiérarcchiquement (mere avant fille)
  397. for ($i=0; $i<$listNodes->length; $i++) {
  398. $node = $listNodes->item($i);
  399. switch ($node->nodeName) {
  400. case 'imsss:rollupRule':
  401. $indice++;
  402. $rules[$indice] = array();
  403. // si pas de valeur trouvée, il faut donner la valeur par défaut
  404. $rules[$indice]['childActivitySet'] = $node->getAttribute('childActivitySet') ??
  405. SCORMTools::getAttributeDefaultValue('childActivitySet');
  406. $rules[$indice]['minimumCount'] = $node->getAttribute('minimumCount') ??
  407. SCORMTools::getAttributeDefaultValue('minimumCount');
  408. $rules[$indice]['minimumPercent'] = $node->getAttribute('minimumPercent') ??
  409. SCORMTools::getAttributeDefaultValue('minimumPercent');
  410. break;
  411. case 'imsss:rollupConditions':
  412. $rules[$indice]['conditionCombination'] = $node->getAttribute('conditionCombination') ??
  413. SCORMTools::getAttributeDefaultValue('imsss:rollupConditions@conditionCombination');
  414. break;
  415. case 'imsss:rollupCondition':
  416. $theAttributes = array();
  417. // il faut initialiser certains attributs avec la valeur par défaut
  418. $theAttributes['operator'] = SCORMTools::getAttributeDefaultValue('operator');
  419. for ($j=0; $j<$node->attributes->length; $j++) {
  420. $nodeAttribute = $node->attributes->item($j);
  421. $theAttributes[$nodeAttribute->nodeName] = $nodeAttribute->nodeValue;
  422. }
  423. $rules[$indice]['conditions'][] = $theAttributes;
  424. break;
  425. case 'imsss:rollupAction':
  426. $rules[$indice]['action']= $node->getAttribute('action');
  427. break;
  428. }
  429. }
  430. }
  431. /**
  432. * renvoi les règles sous forme d'un tableau
  433. *
  434. * Exemple de retour:
  435. *
  436. * 0 => [
  437. * 'conditionCombination' => 'any',
  438. * 'conditions' => [
  439. * 0 => [
  440. * 'operator' => 'not',
  441. * 'referencedObjective' => 'previous_sco_satisfied',
  442. * 'condition' => 'satisfied'
  443. * ],
  444. * 1 => [
  445. * 'operator' => 'not',
  446. * 'referencedObjective' => 'previous_sco_satisfied',
  447. * 'condition' => 'objectiveStatusKnown'
  448. * ]
  449. * ],
  450. * 'action' => 'disabled'
  451. * ]
  452. * @param string $item
  453. * @return array
  454. */
  455. public function getSequencingRules(string $item) : array
  456. {
  457. $rules = array();
  458. // on commence par recuperer (si elles existent) les valeurs sur le sequencing collection
  459. $refSeqCol = $this->getAttributeByName($item, 'IDRef');
  460. if ($refSeqCol != null) {
  461. $query = 'imsss:sequencingCollection/imsss:sequencing[@ID="'.$refSeqCol.'"]/imsss:sequencingRules/descendant::*';
  462. $listNodes = $this->xpath->query($query);
  463. $this->nodesToArray($listNodes, $rules);
  464. }
  465. // recuperation des rules de la sequence de l'item
  466. $query = '//*[name()="item" or name()="organization"][@identifier="'.
  467. $item.'"]/imsss:sequencing/imsss:sequencingRules/descendant::*';
  468. $listNodes = $this->xpath->query($query);
  469. $this->nodesToArray($listNodes, $rules);
  470. return $rules;
  471. }
  472. /**
  473. * transforme une liste de node ordonnées en tableau associatif
  474. * ayant le format commode pour le traitement
  475. *
  476. * node ordonnée: dans le sens de lecture de découverte des nodes dans le manifest.
  477. * l'algo simmplicisme (parcours d'une boucle) est bati sur ce principe.
  478. * (voir exemple de format dans la fonction 'public' appelante)
  479. *
  480. * @param \DOMNodeList $listNodes
  481. * @param array $rules
  482. * @return void
  483. */
  484. private function nodesToArray(\DOMNodeList $listNodes, array &$rules) : void
  485. {
  486. $indice = -1; // indices du plus haut niveau du tableau de rule...
  487. // dans cette boucle on joue sur le fait que les tag sont ordonnés hiérarcchiquement (mere avant fille)
  488. for ($i=0; $i<$listNodes->length; $i++) {
  489. $node = $listNodes->item($i);
  490. switch ($node->nodeName) {
  491. case 'imsss:preConditionRule':
  492. case 'imsss:exitConditionRule':
  493. case 'imsss:postConditionRule':
  494. $indice++;
  495. $rules[$indice] = array();
  496. break;
  497. case 'imsss:ruleCondition':
  498. $theAttributes = array();
  499. // il faut initialiser certains attributs avec la valeur par défaut
  500. $theAttributes['operator'] = SCORMTools::getAttributeDefaultValue('operator');
  501. for ($j=0; $j<$node->attributes->length; $j++) {
  502. $nodeAttribute = $node->attributes->item($j);
  503. $theAttributes[$nodeAttribute->nodeName] = $nodeAttribute->nodeValue;
  504. }
  505. $rules[$indice]['conditions'][] = $theAttributes;
  506. break;
  507. case 'imsss:ruleConditions':
  508. $rules[$indice]['conditionCombination']= $node->getAttribute('conditionCombination') ??
  509. SCORMTools::getAttributeDefaultValue('imsss:ruleConditions@conditionCombination');
  510. break;
  511. case 'imsss:ruleAction':
  512. $rules[$indice]['action']= $node->getAttribute('action');
  513. break;
  514. }
  515. }
  516. }
  517. /**
  518. * renvoie la valeur d'un attribut. Appel getAttributesByName
  519. * et renvoie le seul élement trouvé, s'il y en a plusieurs renvoi null.
  520. *
  521. * @param string $item
  522. * @param string $attributeName
  523. * @return string|null
  524. */
  525. public function getAttributeByName(string $item, string $attributeName): ?string
  526. {
  527. $value = null;
  528. $values = $this->getAttributesByName($item, $attributeName);
  529. if (sizeof($values) == 1) {
  530. $value = $values[0];
  531. }
  532. return $value;
  533. }
  534. /**
  535. * renvoie les valeurs des attributs d'un meme nom.
  536. * scrute tous les attributs de tous les tags descendants de l'item courrant
  537. * y compris l'item courrant SAUF les items descendants.
  538. * ATTENTION: ne suit pas les references
  539. *
  540. * @param string $item nom de l'item racine
  541. * @param string $attributeName nom de l'attribut
  542. * @return array tableau de string contenant les valeurs des attributs dans l'ordre de découverte
  543. */
  544. public function getAttributesByName(string $item, string $attributeName): array
  545. {
  546. $values = array();
  547. $itemNode = $this->getItemByIdentifier($item); // la recherche sse fait au niveau de l'item
  548. // onrecherche la presence de l'attribut dans tous les tags descendant sauf les items enfants
  549. $query = 'descendant::*[name() != "item" and @'.$attributeName.'] | self::*[@'.$attributeName.']';
  550. $nodeList = $this->xpath->query($query, $itemNode);
  551. for ($i=0; $i<$nodeList->length; $i++) {
  552. $node = $nodeList->item($i);
  553. $values[] = $node->getAttribute($attributeName);
  554. }
  555. return $values;
  556. }
  557. /**
  558. * l'organization racine de l'item (ou de l'organization c'est à dire elle meme dans ce cas)
  559. *
  560. * @param string $item
  561. * @return string identifer de l'organization mere de l'item
  562. */
  563. public function getOrganizationByItem(string $item) : string
  564. {
  565. $value ="";
  566. $query = '//*[name()="item" or name()="organization"][@identifier="'.
  567. $item.'"]/ancestor-or-self::*[name()="organization"]';
  568. $nodeList = $this->xpath->query($query);
  569. if ($nodeList->length > 0) {
  570. $node = $nodeList->item(0);
  571. $attributeNode = $node->attributes->getNamedItem('identifier');
  572. if ($attributeNode !=null) {
  573. $value = $attributeNode->nodeValue;
  574. }
  575. }
  576. return $value;
  577. }
  578. /**
  579. * évalue si l'item est present dans la branche constituée de la listes des items partant de
  580. * l'item racine (l'organization) jusqu'à l'item itemBranche
  581. *
  582. * @param string $item
  583. * @param string $itemBranche
  584. * @return boolean true si item est dans la branche
  585. */
  586. public function isItemInBranch(string $item, string $itemBranche) : bool
  587. {
  588. // on remonte jusqu'à trouver l'item ou jusqu'au noeud mere
  589. $currentNode = $this->manifest->getElementById($itemBranche);
  590. do {
  591. $current = $currentNode->attributes->getNamedItem("identifier")->nodeValue;
  592. if ($current == $item) {
  593. return true;
  594. }
  595. $currentNode = $currentNode->parentNode;
  596. } while (($currentNode->nodeName == 'item')||($currentNode->nodeName == 'organization'));
  597. return false;
  598. }
  599. /**
  600. * @deprecated use getItemBranch
  601. *
  602. * @param string $item
  603. * @return array
  604. */
  605. public function getNodeBranch(string $item) : array
  606. {
  607. return getItemBranch($item);
  608. }
  609. /**
  610. * renvoie la liste ordonnée de la racine (l'organization) à l'item de fin de branche
  611. *
  612. * @param string $item fin de branche
  613. * @return array tableau associatif demarrant à l' "item" organization
  614. */
  615. public function getItemBranch(string $item) : array
  616. {
  617. // remontée de la branche jusqu'à la racine puis inversion du tableau
  618. $branche = array();
  619. $currentNode = $this->manifest->getElementById($item);
  620. do {
  621. $current = $currentNode->attributes->getNamedItem("identifier")->nodeValue;
  622. $branche[$current] = true;
  623. $currentNode = $currentNode->parentNode;
  624. } while (($currentNode->nodeName == 'item') || ($currentNode->nodeName == 'organization'));
  625. $branche = array_reverse($branche);
  626. return $branche;
  627. }
  628. /**
  629. * renvoi la liste des objectif (objectiveID) d'un item
  630. * la forme d'un objective est le triplet (identifier (item),objectiveID, isPrimary)
  631. *
  632. * @param string $item
  633. * @return array de tableaux d'"objective"s
  634. */
  635. public function getObjectives(string $item) : array
  636. {
  637. $ids = array();
  638. // recuperation des rules de la sequence de l'item
  639. $query = '//*[name()="item" or name()="organization"][@identifier="'.
  640. $item.'"]/imsss:sequencing/imsss:objectives/descendant::*[@objectiveID] | '.
  641. '//imsss:sequencingCollection/imsss:sequencing/imsss:objectives/descendant::*[@objectiveID]';
  642. $listNodes = $this->xpath->query($query);
  643. for ($i=0; $i<$listNodes->length; $i++) {
  644. $ids[] = $this->constructObjective(
  645. $item,
  646. $listNodes->item($i)->getAttribute('objectiveID'),
  647. $listNodes->item($i)->nodeName
  648. );
  649. }
  650. return $ids;
  651. }
  652. /**
  653. * recupere le triplet definnissant un PrimaryObjective l'attribut imsss:primaryObjective
  654. * la forme d'un objective est le triplet (identifier (item),objectiveID, isPrimary)
  655. * @param string $item
  656. * @return array|null
  657. */
  658. public function getPrimaryObjective(string $item) : ?array
  659. {
  660. $o = null;
  661. $query = '//*[name()="item" or name()="organization"][@identifier="'.
  662. $item.'"]/imsss:sequencing/imsss:objectives/imsss:primaryObjective';
  663. $listNodes = $this->xpath->query($query);
  664. if ($listNodes->length == 1) {
  665. $o = $this->constructObjective(
  666. $item,
  667. $listNodes->item(0)->getAttribute('objectiveID'),
  668. $listNodes->item(0)->nodeName
  669. );
  670. }
  671. return $o;
  672. }
  673. /**
  674. * un bon carcan pour construire à l'identique le tableau (on evite la Class spécifique)
  675. *
  676. * @param string $item
  677. * @param string $objectiveID
  678. * @param string $objectiveTagName
  679. * @return void
  680. */
  681. private function constructObjective(string $item, string $objectiveID, string $objectiveTagName)
  682. {
  683. return array(
  684. 'item' => $item,
  685. 'objective' => $objectiveID,
  686. 'isPrimary' => ($objectiveTagName == 'imsss:primaryObjective') ? true : false
  687. );
  688. }
  689. /**
  690. * recupere la valeur du tag minNormalizedMeasure
  691. *
  692. * @param string $item
  693. * @param string $objectiveID identifiant de l'ojectif contenant le tag
  694. * @return string|null
  695. */
  696. public function getMinNormalizeMeasure(string $item, string $objectiveID) : ?string
  697. {
  698. $value = null;
  699. $query = '//*[name()="item" or name()="organization"][@identifier="'.
  700. $item.'"]/imsss:sequencing/imsss:objectives/*[@objectiveID="'.$objectiveID.'"]/imsss:minNormalizedMeasure';
  701. $listNodes = $this->xpath->query($query);
  702. if ($listNodes->length == 1) {
  703. $value = $listNodes->item(0)->nodeValue;
  704. }
  705. return $value;
  706. }
  707. /**
  708. * En preambule:
  709. * tous les objectives tag : "objective" porteurs
  710. * d'une mapInfo ont obligatoirement un objectiveID associé (CAM5-26 )
  711. * les tags "primaryObjective" n'ont pas pas focement un objectiveID
  712. *
  713. * IMPORTANT : apparement un objective peut avoir plusieurs mapInfo
  714. * ce cas n'est pas traité et génère une exception!!!
  715. *
  716. * @param string $item
  717. * @param string $objectiveID peut etre chaine vide si et seulement $isPrimary = true
  718. * @param boolean $isPrimary
  719. * @return void
  720. */
  721. public function getMapInfos(string $item, string $objectiveID, bool $isPrimary)
  722. {
  723. $mapInfo = array( // tous les attributs que nous souhaitons retourner obligatoirement...
  724. 'targetObjectiveID' => null,
  725. 'readSatisfiedStatus' => null,
  726. 'readNormalizedMeasure' => null,
  727. 'writeSatisfiedStatus' => null,
  728. 'writeNormalizedMeasure' => null,
  729. 'readRawScore' => null,
  730. 'readMinScore' => null,
  731. 'readMaxScore' => null,
  732. 'readCompletionStatus' => null,
  733. 'readProgressMeasure' => null,
  734. 'writeRawScore' => null,
  735. 'writeMinScore' => null,
  736. 'writeMaxScore' => null,
  737. 'writeCompletionStatus' => null,
  738. 'writeProgressMeasure' => null
  739. );
  740. if ($isPrimary == false) {
  741. $query = '//*[name()="item" or name()="organization"][@identifier="'.
  742. $item.'"]/imsss:sequencing/imsss:objectives/imsss:objective[@objectiveID="'.$objectiveID.'"]/imsss:mapInfo | '.
  743. '//imsss:sequencingCollection/imsss:sequencing/imsss:objectives/imsss:objective[@objectiveID="'.$objectiveID.'"]/imsss:mapInfo';
  744. } else {
  745. $query = '//*[name()="item" or name()="organization"][@identifier="'.
  746. $item.'"]/imsss:sequencing/imsss:objectives/imsss:primaryObjective/imsss:mapInfo | '.
  747. '//imsss:sequencingCollection/imsss:sequencing/imsss:objectives/imsss:primaryObjective/imsss:mapInfo';
  748. }
  749. $listNodes = $this->xpath->query($query);
  750. if ($listNodes->length > 1) { // on ne sait pas gérer le cas de plusieurs mapinfo pour 1 objectif
  751. throw new Exception('SCORM 2004: Several mapInfos imsss tags found for item '.$item);
  752. }
  753. if ($listNodes->length == 1) {
  754. $attributes = $listNodes->item(0)->attributes;
  755. for ($i=0; $i<$attributes->length; $i++) {
  756. $a = $attributes->item($i);
  757. $mapInfo[$a->nodeName] = $a->nodeValue;
  758. }
  759. }
  760. if ($mapInfo['targetObjectiveID'] == null) { // pas d'imsss ? alors il faut tenter adlseq
  761. $query = '//*[name()="item" or name()="organization"][@identifier="'.
  762. $item.'"]/imsss:sequencing/adlseq:objectives/*[@objectiveID="'.$objectiveID.'"]/adlseq:mapInfo | '.
  763. '//imsss:sequencingCollection/imsss:sequencing/adlseq:objectives/*[@objectiveID="'.$objectiveID.'"]/adlseq:mapInfo';
  764. } else {
  765. // recherche du complement adlseq
  766. $query = '//*[name()="item" or name()="organization"][@identifier="'.
  767. $item.'"]/imsss:sequencing/adlseq:objectives/child::*/adlseq:mapInfo[@targetObjectiveID="'.$mapInfo['targetObjectiveID'].'"] | '.
  768. '//imsss:sequencingCollection/adlseq:objectives/child::*/adlseq:mapInfo[@targetObjectiveID="'.$mapInfo['targetObjectiveID'].'"]';
  769. }
  770. // on cherche ailleurs que dans le cas ou on n'a un primaryObjective sans objectiveID (seul cas ou objectiveID peut etre null)
  771. if ($objectiveID != "") {
  772. $listNodes = $this->xpath->query($query);
  773. if ($listNodes->length > 1) { // on ne sait pas gérer le cas de plusieurs mapinfo pour 1 objectif
  774. throw new Exception('SCORM 2004: Several mapInfos adlseq tags found for item '.$item);
  775. }
  776. if ($listNodes->length == 1) {
  777. $attributes = $listNodes->item(0)->attributes;
  778. for ($i=0; $i<$attributes->length; $i++) {
  779. $a = $attributes->item($i);
  780. $mapInfo[$a->nodeName] = $a->nodeValue;
  781. }
  782. }
  783. }
  784. foreach ($mapInfo as $key => $value) {
  785. if ($value == null) {
  786. $mapInfo[$key] = SCORMTools::getAttributeDefaultValue($key);
  787. }
  788. }
  789. return $mapInfo;
  790. }
  791. /**
  792. * renvoie l'attribut satisfiedByMeasure accroché à un "(primary)Objective"
  793. *
  794. * @param string $item
  795. * @param string $objectiveID
  796. * @param boolean $isPrimary
  797. * @return string
  798. */
  799. public function getSatisfiedByMeasure(string $item, string $objectiveID, bool $isPrimary) : string
  800. {
  801. $value = null;
  802. if ($isPrimary == false) {
  803. $query = '//*[name()="item" or name()="organization"][@identifier="'.
  804. $item.'"]/imsss:sequencing/imsss:objectives/imsss:objective[@objectiveID="'.$objectiveID.'"] | '.
  805. '//imsss:sequencingCollection/imsss:sequencing/imsss:objectives/imsss:objective[@objectiveID="'.$objectiveID.'"]';
  806. } else { // en cas de primary on ne s'occupe pas de la valeur de l'objectiveID (qui pourrait etre "")
  807. $query = '//*[name()="item" or name()="organization"][@identifier="'.
  808. $item.'"]/imsss:sequencing/imsss:objectives/imsss:primaryObjective | '.
  809. '//imsss:sequencingCollection/imsss:sequencing/imsss:objectives/imsss:primaryObjective';
  810. }
  811. $listNodes = $this->xpath->query($query);
  812. if ($listNodes->length == 1) {
  813. $value = $listNodes->item(0)->getAttribute('satisfiedByMeasure');
  814. }
  815. $value = $value ?? SCORMTools::getAttributeDefaultValue('satisfiedByMeasure');
  816. return $value;
  817. }
  818. /**
  819. * attribut de la balise organization 'adlseq:objectivesGlobalToSystem' (valeur par défaut : true)
  820. *
  821. * @param string $identifier identifient eventuelle de l'organization (si absent organisation par defaut)
  822. * @return string
  823. */
  824. public function getIsGlobalToSystem(string $identifier = null) : string
  825. {
  826. $value = null;
  827. if ($identifier != null) {
  828. $query = '//*[name()="organization"][@identifier="'.$identifier.'"]';
  829. } else {
  830. $query = '//*[name()="organization"][1]'; //selection du &er element
  831. }
  832. $listNodes = $this->xpath->query($query);
  833. if ($listNodes->length == 1) {
  834. $value = $listNodes->item(0)->getAttribute('adlseq:objectivesGlobalToSystem');
  835. }
  836. $value = $value ?? SCORMTools::getAttributeDefaultValue('adlseq:objectivesGlobalToSystem');
  837. return $value;
  838. }
  839. /**
  840. * l'objective défini par son identifiant et son item est il primaire ?
  841. *
  842. * @param string $item
  843. * @param string $objectiveID
  844. * @return boolean true si primaire, false si pas primaire ou n'existe pas
  845. */
  846. public function checkIfPrimary(string $item, string $objectiveID) : bool
  847. {
  848. $isPrimary = false;
  849. // inutile de se préoccuper d'adlseq qui ne contient pas de primary
  850. $query = '//*[name()="item" or name()="organization"][@identifier="'.
  851. $item.'"]/imsss:sequencing/imsss:objectives/child::*[@objectiveID="'.$objectiveID.'"] | '.
  852. '//imsss:sequencingCollection/imsss:sequencing/imsss:objectives/child::*[@objectiveID="'.$objectiveID.'"]';
  853. $listNodes = $this->xpath->query($query);
  854. if ($listNodes->length == 1) {
  855. $isPrimary = $listNodes->item(0)->nodeName == 'imsss:primaryObjective';
  856. }
  857. return $isPrimary;
  858. }
  859. /**
  860. * Retourne le standard du paquet associé au DOM
  861. * Méthode surchargée
  862. *
  863. * @return string
  864. */
  865. public function getStandard()
  866. {
  867. return DOMSCORM::SCORM_2004;
  868. }
  869. }