summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Entity/Sco.php270
-rw-r--r--src/Entity/ScoTracking.php343
-rw-r--r--src/Entity/Scorm.php146
-rw-r--r--src/Exception/InvalidScormArchiveException.php11
-rw-r--r--src/Exception/StorageNotFoundException.php11
-rw-r--r--src/Facade/ScormManager.php14
-rw-r--r--src/Library/ScormLib.php253
-rw-r--r--src/Manager/ScormManager.php587
-rw-r--r--src/Model/ScormModel.php19
-rw-r--r--src/Model/ScormScoModel.php19
-rw-r--r--src/Model/ScormScoTrackingModel.php42
-rw-r--r--src/ScormServiceProvider.php62
12 files changed, 1777 insertions, 0 deletions
diff --git a/src/Entity/Sco.php b/src/Entity/Sco.php
new file mode 100644
index 0000000..75eec2b
--- /dev/null
+++ b/src/Entity/Sco.php
@@ -0,0 +1,270 @@
+<?php
+
+
+namespace Peopleaps\Scorm\Entity;
+
+
+class Sco
+{
+ public $id;
+ public $uuid;
+ public $scorm;
+ public $scoParent;
+ public $scoChildren;
+ public $entryUrl;
+ public $identifier;
+ public $title;
+ public $visible;
+ public $parameters;
+ public $launchData;
+ public $maxTimeAllowed;
+ public $timeLimitAction;
+ public $block;
+ public $scoreToPassInt;
+ public $scoreToPassDecimal;
+ public $completionThreshold;
+ public $prerequisites;
+
+ public function getData() {
+ return [
+ 'id' => $this->getId(),
+ 'scorm' => $this->getScorm(),
+ 'scoParent' => $this->getScoParent(),
+ 'entryUrl' => $this->getEntryUrl(),
+ 'identifier' => $this->getIdentifier(),
+
+ ];
+ }
+
+ public function getUuid()
+ {
+ return $this->uuid;
+ }
+
+ public function setUuid($uuid)
+ {
+ $this->uuid = $uuid;
+ }
+
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ public function setId($id)
+ {
+ $this->id = $id;
+ }
+
+ public function getScorm()
+ {
+ return $this->scorm;
+ }
+
+ public function setScorm(Scorm $scorm = null)
+ {
+ $this->scorm = $scorm;
+ }
+
+ public function getScoParent()
+ {
+ return $this->scoParent;
+ }
+
+ public function setScoParent(Sco $scoParent = null)
+ {
+ $this->scoParent = $scoParent;
+ }
+
+ public function getScoChildren()
+ {
+ return $this->scoChildren;
+ }
+
+ public function setScoChildren($scoChildren)
+ {
+ $this->scoChildren = $scoChildren;
+ }
+
+ public function getEntryUrl()
+ {
+ return $this->entryUrl;
+ }
+
+ public function setEntryUrl($entryUrl)
+ {
+ $this->entryUrl = $entryUrl;
+ }
+
+ public function getIdentifier()
+ {
+ return $this->identifier;
+ }
+
+ public function setIdentifier($identifier)
+ {
+ $this->identifier = $identifier;
+ }
+
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ }
+
+ public function isVisible()
+ {
+ return $this->visible;
+ }
+
+ public function setVisible($visible)
+ {
+ $this->visible = $visible;
+ }
+
+ public function getParameters()
+ {
+ return $this->parameters;
+ }
+
+ public function setParameters($parameters)
+ {
+ $this->parameters = $parameters;
+ }
+
+ public function getLaunchData()
+ {
+ return $this->launchData;
+ }
+
+ public function setLaunchData($launchData)
+ {
+ $this->launchData = $launchData;
+ }
+
+ public function getMaxTimeAllowed()
+ {
+ return $this->maxTimeAllowed;
+ }
+
+ public function setMaxTimeAllowed($maxTimeAllowed)
+ {
+ $this->maxTimeAllowed = $maxTimeAllowed;
+ }
+
+ public function getTimeLimitAction()
+ {
+ return $this->timeLimitAction;
+ }
+
+ public function setTimeLimitAction($timeLimitAction)
+ {
+ $this->timeLimitAction = $timeLimitAction;
+ }
+
+ public function isBlock()
+ {
+ return $this->block;
+ }
+
+ public function setBlock($block)
+ {
+ $this->block = $block;
+ }
+
+ public function getScoreToPass()
+ {
+ if (Scorm::SCORM_2004 === $this->scorm->getVersion()) {
+ return $this->scoreToPassDecimal;
+ } else {
+ return $this->scoreToPassInt;
+ }
+ }
+
+ public function setScoreToPass($scoreToPass)
+ {
+ if (Scorm::SCORM_2004 === $this->scorm->getVersion()) {
+ $this->setScoreToPassDecimal($scoreToPass);
+ } else {
+ $this->setScoreToPassInt($scoreToPass);
+ }
+ }
+
+ public function getScoreToPassInt()
+ {
+ return $this->scoreToPassInt;
+ }
+
+ public function setScoreToPassInt($scoreToPassInt)
+ {
+ $this->scoreToPassInt = $scoreToPassInt;
+ }
+
+ public function getScoreToPassDecimal()
+ {
+ return $this->scoreToPassDecimal;
+ }
+
+ public function setScoreToPassDecimal($scoreToPassDecimal)
+ {
+ $this->scoreToPassDecimal = $scoreToPassDecimal;
+ }
+
+ public function getCompletionThreshold()
+ {
+ return $this->completionThreshold;
+ }
+
+ public function setCompletionThreshold($completionThreshold)
+ {
+ $this->completionThreshold = $completionThreshold;
+ }
+
+ public function getPrerequisites()
+ {
+ return $this->prerequisites;
+ }
+
+ public function setPrerequisites($prerequisites)
+ {
+ $this->prerequisites = $prerequisites;
+ }
+
+ /**
+ * @return array
+ */
+ public function serialize(Sco $sco)
+ {
+ $scorm = $sco->getScorm();
+ $parent = $sco->getScoParent();
+
+ return [
+ 'id' => $sco->getUuid(),
+ 'scorm' => !empty($scorm) ? ['id' => $scorm->getUuid()] : null,
+ 'data' => [
+ 'entryUrl' => $sco->getEntryUrl(),
+ 'identifier' => $sco->getIdentifier(),
+ 'title' => $sco->getTitle(),
+ 'visible' => $sco->isVisible(),
+ 'parameters' => $sco->getParameters(),
+ 'launchData' => $sco->getLaunchData(),
+ 'maxTimeAllowed' => $sco->getMaxTimeAllowed(),
+ 'timeLimitAction' => $sco->getTimeLimitAction(),
+ 'block' => $sco->isBlock(),
+ 'scoreToPassInt' => $sco->getScoreToPassInt(),
+ 'scoreToPassDecimal' => $sco->getScoreToPassDecimal(),
+ 'scoreToPass' => !empty($scorm) ? $sco->getScoreToPass() : null,
+ 'completionThreshold' => $sco->getCompletionThreshold(),
+ 'prerequisites' => $sco->getPrerequisites(),
+ ],
+ 'parent' => !empty($parent) ? ['id' => $parent->getUuid()] : null,
+ 'children' => array_map(function (Sco $scoChild) {
+ return $this->serialize($scoChild);
+ }, is_array($sco->getScoChildren()) ? $sco->getScoChildren() : $sco->getScoChildren()->toArray()),
+ ];
+ }
+}
diff --git a/src/Entity/ScoTracking.php b/src/Entity/ScoTracking.php
new file mode 100644
index 0000000..c0b001d
--- /dev/null
+++ b/src/Entity/ScoTracking.php
@@ -0,0 +1,343 @@
+<?php
+
+
+namespace Peopleaps\Scorm\Entity;
+
+
+class ScoTracking
+{
+ public $userId;
+ public $sco;
+ public $scoreRaw;
+ public $scoreMin;
+ public $scoreMax;
+ public $progression = 0;
+ public $scoreScaled;
+ public $lessonStatus;
+ public $completionStatus;
+ public $sessionTime;
+ public $totalTimeInt;
+ public $totalTimeString;
+ public $entry;
+ public $suspendData;
+ public $credit;
+ public $exitMode;
+ public $lessonLocation;
+ public $lessonMode;
+ public $isLocked;
+ public $details;
+ public $latestDate;
+
+ public function getUserId()
+ {
+ return $this->userId;
+ }
+
+ public function setUserId($userId)
+ {
+ $this->userId = $userId;
+ }
+
+ /**
+ * @return Sco
+ */
+ public function getSco()
+ {
+ return $this->sco;
+ }
+
+ public function setSco($sco)
+ {
+ $this->sco = $sco;
+ }
+
+ public function getScoreRaw()
+ {
+ return $this->scoreRaw;
+ }
+
+ public function setScoreRaw($scoreRaw)
+ {
+ $this->scoreRaw = $scoreRaw;
+ }
+
+ public function getScoreMin()
+ {
+ return $this->scoreMin;
+ }
+
+ public function setScoreMin($scoreMin)
+ {
+ $this->scoreMin = $scoreMin;
+ }
+
+ public function getScoreMax()
+ {
+ return $this->scoreMax;
+ }
+
+ public function setScoreMax($scoreMax)
+ {
+ $this->scoreMax = $scoreMax;
+ }
+
+ public function getScoreScaled()
+ {
+ return $this->scoreScaled;
+ }
+
+ public function setScoreScaled($scoreScaled)
+ {
+ $this->scoreScaled = $scoreScaled;
+ }
+
+ public function getLessonStatus()
+ {
+ return $this->lessonStatus;
+ }
+
+ public function setLessonStatus($lessonStatus)
+ {
+ $this->lessonStatus = $lessonStatus;
+ }
+
+ public function getCompletionStatus()
+ {
+ return $this->completionStatus;
+ }
+
+ public function setCompletionStatus($completionStatus)
+ {
+ $this->completionStatus = $completionStatus;
+ }
+
+ public function getSessionTime()
+ {
+ return $this->sessionTime;
+ }
+
+ public function setSessionTime($sessionTime)
+ {
+ $this->sessionTime = $sessionTime;
+ }
+
+ public function getTotalTime($scormVersion)
+ {
+ if (Scorm::SCORM_2004 === $scormVersion) {
+ return $this->totalTimeString;
+ } else {
+ return $this->totalTimeInt;
+ }
+ }
+
+ public function setTotalTime($totalTime, $scormVersion)
+ {
+ if (Scorm::SCORM_2004 === $scormVersion) {
+ $this->setTotalTimeString($totalTime);
+ } else {
+ $this->setTotalTimeInt($totalTime);
+ }
+ }
+
+ public function getTotalTimeInt()
+ {
+ return $this->totalTimeInt;
+ }
+
+ public function setTotalTimeInt($totalTimeInt)
+ {
+ $this->totalTimeInt = $totalTimeInt;
+ }
+
+ public function getTotalTimeString()
+ {
+ return $this->totalTimeString;
+ }
+
+ public function setTotalTimeString($totalTimeString)
+ {
+ $this->totalTimeString = $totalTimeString;
+ }
+
+ public function getEntry()
+ {
+ return $this->entry;
+ }
+
+ public function setEntry($entry)
+ {
+ $this->entry = $entry;
+ }
+
+ public function getSuspendData()
+ {
+ return $this->suspendData;
+ }
+
+ public function setSuspendData($suspendData)
+ {
+ $this->suspendData = $suspendData;
+ }
+
+ public function getCredit()
+ {
+ return $this->credit;
+ }
+
+ public function setCredit($credit)
+ {
+ $this->credit = $credit;
+ }
+
+ public function getExitMode()
+ {
+ return $this->exitMode;
+ }
+
+ public function setExitMode($exitMode)
+ {
+ $this->exitMode = $exitMode;
+ }
+
+ public function getLessonLocation()
+ {
+ return $this->lessonLocation;
+ }
+
+ public function setLessonLocation($lessonLocation)
+ {
+ $this->lessonLocation = $lessonLocation;
+ }
+
+ public function getLessonMode()
+ {
+ return $this->lessonMode;
+ }
+
+ public function setLessonMode($lessonMode)
+ {
+ $this->lessonMode = $lessonMode;
+ }
+
+ public function getIsLocked()
+ {
+ return $this->isLocked;
+ }
+
+ public function setIsLocked($isLocked)
+ {
+ $this->isLocked = $isLocked;
+ }
+
+ public function getDetails()
+ {
+ return $this->details;
+ }
+
+ public function setDetails($details)
+ {
+ $this->details = $details;
+ }
+
+ public function getLatestDate()
+ {
+ return $this->latestDate;
+ }
+
+ public function setLatestDate(\DateTime $latestDate = null)
+ {
+ $this->latestDate = $latestDate;
+ }
+
+ public function getProgression()
+ {
+ return $this->progression;
+ }
+
+ public function setProgression($progression)
+ {
+ $this->progression = $progression;
+ }
+
+ public function getFormattedTotalTime()
+ {
+ if (Scorm::SCORM_2004 === $this->sco->getScorm()->getVersion()) {
+ return $this->getFormattedTotalTimeString();
+ } else {
+ return $this->getFormattedTotalTimeInt();
+ }
+ }
+
+ public function getFormattedTotalTimeInt()
+ {
+ $remainingTime = $this->totalTimeInt;
+ $hours = intval($remainingTime / 360000);
+ $remainingTime %= 360000;
+ $minutes = intval($remainingTime / 6000);
+ $remainingTime %= 6000;
+ $seconds = intval($remainingTime / 100);
+ $remainingTime %= 100;
+
+ $formattedTime = '';
+
+ if ($hours < 10) {
+ $formattedTime .= '0';
+ }
+ $formattedTime .= $hours.':';
+
+ if ($minutes < 10) {
+ $formattedTime .= '0';
+ }
+ $formattedTime .= $minutes.':';
+
+ if ($seconds < 10) {
+ $formattedTime .= '0';
+ }
+ $formattedTime .= $seconds.'.';
+
+ if ($remainingTime < 10) {
+ $formattedTime .= '0';
+ }
+ $formattedTime .= $remainingTime;
+
+ return $formattedTime;
+ }
+
+ public function getFormattedTotalTimeString()
+ {
+ $pattern = '/^P([0-9]+Y)?([0-9]+M)?([0-9]+D)?T([0-9]+H)?([0-9]+M)?([0-9]+S)?$/';
+ $formattedTime = '';
+
+ if (!empty($this->totalTimeString) && 'PT' !== $this->totalTimeString && preg_match($pattern, $this->totalTimeString)) {
+ $interval = new \DateInterval($this->totalTimeString);
+ $time = new \DateTime();
+ $time->setTimestamp(0);
+ $time->add($interval);
+ $timeInSecond = $time->getTimestamp();
+
+ $hours = intval($timeInSecond / 3600);
+ $timeInSecond %= 3600;
+ $minutes = intval($timeInSecond / 60);
+ $timeInSecond %= 60;
+
+ if ($hours < 10) {
+ $formattedTime .= '0';
+ }
+ $formattedTime .= $hours.':';
+
+ if ($minutes < 10) {
+ $formattedTime .= '0';
+ }
+ $formattedTime .= $minutes.':';
+
+ if ($timeInSecond < 10) {
+ $formattedTime .= '0';
+ }
+ $formattedTime .= $timeInSecond;
+ } else {
+ $formattedTime .= '00:00:00';
+ }
+
+ return $formattedTime;
+ }
+}
diff --git a/src/Entity/Scorm.php b/src/Entity/Scorm.php
new file mode 100644
index 0000000..4095986
--- /dev/null
+++ b/src/Entity/Scorm.php
@@ -0,0 +1,146 @@
+<?php
+
+
+namespace Peopleaps\Scorm\Entity;
+
+
+use Doctrine\Common\Collections\ArrayCollection;
+
+class Scorm
+{
+ const SCORM_12 = 'scorm_12';
+ const SCORM_2004 = 'scorm_2004';
+
+ public $uuid;
+ public $id;
+ public $version;
+ public $hashName;
+ public $ratio = 56.25;
+ public $scos;
+ public $scoSerializer;
+
+ /**
+ * @return string
+ */
+ public function getUuid()
+ {
+ return $this->uuid;
+ }
+
+ /**
+ * @param int $id
+ */
+ public function setUuid($uuid)
+ {
+ $this->uuid = $uuid;
+ }
+
+ /**
+ * @return int
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @param int $id
+ */
+ public function setId($id)
+ {
+ $this->id = $id;
+ }
+
+ /**
+ * @return string
+ */
+ public function getVersion()
+ {
+ return $this->version;
+ }
+
+ /**
+ * @param string $version
+ */
+ public function setVersion($version)
+ {
+ $this->version = $version;
+ }
+
+ /**
+ * @return string
+ */
+ public function getHashName()
+ {
+ return $this->hashName;
+ }
+
+ /**
+ * @param string $hashName
+ */
+ public function setHashName($hashName)
+ {
+ $this->hashName = $hashName;
+ }
+
+ /**
+ * @return float
+ */
+ public function getRatio()
+ {
+ return $this->ratio;
+ }
+
+ /**
+ * @param float $ratio
+ */
+ public function setRatio($ratio)
+ {
+ $this->ratio = $ratio;
+ }
+
+ /**
+ * @return Sco[]
+ */
+ public function getScos()
+ {
+ return $this->scos;
+ }
+
+ /**
+ * @return Sco[]
+ */
+ public function getRootScos()
+ {
+ $roots = [];
+
+ if (!empty($this->scos)) {
+ foreach ($this->scos as $sco) {
+ if (is_null($sco->getScoParent())) {
+ // Root sco found
+ $roots[] = $sco;
+ }
+ }
+ }
+
+ return $roots;
+ }
+
+ public function serialize(Scorm $scorm)
+ {
+ return [
+ 'id' => $scorm->getUuid(),
+ 'version' => $scorm->getVersion(),
+ 'hashName' => $scorm->getHashName(),
+ 'ratio' => $scorm->getRatio(),
+ 'scos' => $this->serializeScos($scorm),
+ ];
+ }
+
+ private function serializeScos(Scorm $scorm)
+ {
+ return array_map(function (Sco $sco) {
+ return $sco->serialize($sco);
+ }, $scorm->getRootScos());
+ }
+}
diff --git a/src/Exception/InvalidScormArchiveException.php b/src/Exception/InvalidScormArchiveException.php
new file mode 100644
index 0000000..34a3a6c
--- /dev/null
+++ b/src/Exception/InvalidScormArchiveException.php
@@ -0,0 +1,11 @@
+<?php
+
+
+namespace Peopleaps\Scorm\Exception;
+
+use Exception;
+
+class InvalidScormArchiveException extends Exception
+{
+
+}
diff --git a/src/Exception/StorageNotFoundException.php b/src/Exception/StorageNotFoundException.php
new file mode 100644
index 0000000..e657271
--- /dev/null
+++ b/src/Exception/StorageNotFoundException.php
@@ -0,0 +1,11 @@
+<?php
+
+
+namespace Peopleaps\Scorm\Exception;
+
+use Exception;
+
+class StorageNotFoundException extends Exception
+{
+
+}
diff --git a/src/Facade/ScormManager.php b/src/Facade/ScormManager.php
new file mode 100644
index 0000000..fe8cbfe
--- /dev/null
+++ b/src/Facade/ScormManager.php
@@ -0,0 +1,14 @@
+<?php
+
+
+namespace Peopleaps\Scorm\Facade;
+
+use Illuminate\Support\Facades\Facade;
+
+class ScormManager extends Facade
+{
+ protected static function getFacadeAccessor()
+ {
+ return 'scorm-manager';
+ }
+}
diff --git a/src/Library/ScormLib.php b/src/Library/ScormLib.php
new file mode 100644
index 0000000..0ca8a6b
--- /dev/null
+++ b/src/Library/ScormLib.php
@@ -0,0 +1,253 @@
+<?php
+
+
+namespace Peopleaps\Scorm\Library;
+
+
+use DOMDocument;
+use Peopleaps\Scorm\Entity\Sco;
+use Peopleaps\Scorm\Exception\InvalidScormArchiveException;
+use Ramsey\Uuid\Uuid;
+
+class ScormLib
+{
+ /**
+ * Looks for the organization to use.
+ *
+ * @return array of Sco
+ *
+ * @throws InvalidScormArchiveException If a default organization
+ * is defined and not found
+ */
+ public function parseOrganizationsNode(DOMDocument $dom)
+ {
+ $organizationsList = $dom->getElementsByTagName('organizations');
+ $resources = $dom->getElementsByTagName('resource');
+
+ if ($organizationsList->length > 0) {
+ $organizations = $organizationsList->item(0);
+ $organization = $organizations->firstChild;
+
+ if (!is_null($organizations->attributes)
+ && !is_null($organizations->attributes->getNamedItem('default'))) {
+ $defaultOrganization = $organizations->attributes->getNamedItem('default')->nodeValue;
+ } else {
+ $defaultOrganization = null;
+ }
+ // No default organization is defined
+ if (is_null($defaultOrganization)) {
+ while (!is_null($organization)
+ && 'organization' !== $organization->nodeName) {
+ $organization = $organization->nextSibling;
+ }
+
+ if (is_null($organization)) {
+ return $this->parseResourceNodes($resources);
+ }
+ }
+ // A default organization is defined
+ // Look for it
+ else {
+ while (!is_null($organization)
+ && ('organization' !== $organization->nodeName
+ || is_null($organization->attributes->getNamedItem('identifier'))
+ || $organization->attributes->getNamedItem('identifier')->nodeValue !== $defaultOrganization)) {
+ $organization = $organization->nextSibling;
+ }
+
+ if (is_null($organization)) {
+ throw new InvalidScormArchiveException('default_organization_not_found_message');
+ }
+ }
+
+ return $this->parseItemNodes($organization, $resources);
+ } else {
+ throw new InvalidScormArchiveException('no_organization_found_message');
+ }
+ }
+
+ /**
+ * Creates defined structure of SCOs.
+ *
+ * @return array of Sco
+ *
+ * @throws InvalidScormArchiveException
+ */
+ private function parseItemNodes(\DOMNode $source, \DOMNodeList $resources, Sco $parentSco = null)
+ {
+ $item = $source->firstChild;
+ $scos = [];
+
+ while (!is_null($item)) {
+ if ('item' === $item->nodeName) {
+ $sco = new Sco();
+ $scos[] = $sco;
+ $sco->setUuid(Uuid::uuid4());
+ $sco->setScoParent($parentSco);
+ $this->findAttrParams($sco, $item, $resources);
+ $this->findNodeParams($sco, $item->firstChild);
+
+ if ($sco->isBlock()) {
+ $sco->setScoChildren($this->parseItemNodes($item, $resources, $sco));
+ }
+ }
+ $item = $item->nextSibling;
+ }
+
+ return $scos;
+ }
+
+ private function parseResourceNodes(\DOMNodeList $resources)
+ {
+ $scos = [];
+
+ foreach ($resources as $resource) {
+ if (!is_null($resource->attributes)) {
+ $scormType = $resource->attributes->getNamedItemNS(
+ $resource->lookupNamespaceUri('adlcp'),
+ 'scormType'
+ );
+
+ if (!is_null($scormType) && 'sco' === $scormType->nodeValue) {
+ $identifier = $resource->attributes->getNamedItem('identifier');
+ $href = $resource->attributes->getNamedItem('href');
+
+ if (is_null($identifier)) {
+ throw new InvalidScormArchiveException('sco_with_no_identifier_message');
+ }
+ if (is_null($href)) {
+ throw new InvalidScormArchiveException('sco_resource_without_href_message');
+ }
+ $sco = new Sco();
+ $sco->setUuid(Uuid::uuid4());
+ $sco->setBlock(false);
+ $sco->setVisible(true);
+ $sco->setIdentifier($identifier->nodeValue);
+ $sco->setTitle($identifier->nodeValue);
+ $sco->setEntryUrl($href->nodeValue);
+ $scos[] = $sco;
+ }
+ }
+ }
+
+ return $scos;
+ }
+
+ /**
+ * Initializes parameters of the SCO defined in attributes of the node.
+ * It also look for the associated resource if it is a SCO and not a block.
+ *
+ * @throws InvalidScormArchiveException
+ */
+ private function findAttrParams(Sco $sco, \DOMNode $item, \DOMNodeList $resources)
+ {
+ $identifier = $item->attributes->getNamedItem('identifier');
+ $isVisible = $item->attributes->getNamedItem('isvisible');
+ $identifierRef = $item->attributes->getNamedItem('identifierref');
+ $parameters = $item->attributes->getNamedItem('parameters');
+
+ // throws an Exception if identifier is undefined
+ if (is_null($identifier)) {
+ throw new InvalidScormArchiveException('sco_with_no_identifier_message');
+ }
+ $sco->setIdentifier($identifier->nodeValue);
+
+ // visible is true by default
+ if (!is_null($isVisible) && 'false' === $isVisible) {
+ $sco->setVisible(false);
+ } else {
+ $sco->setVisible(true);
+ }
+
+ // set parameters for SCO entry resource
+ if (!is_null($parameters)) {
+ $sco->setParameters($parameters->nodeValue);
+ }
+
+ // check if item is a block or a SCO. A block doesn't define identifierref
+ if (is_null($identifierRef)) {
+ $sco->setBlock(true);
+ } else {
+ $sco->setBlock(false);
+ // retrieve entry URL
+ $sco->setEntryUrl($this->findEntryUrl($identifierRef->nodeValue, $resources));
+ }
+ }
+
+ /**
+ * Initializes parameters of the SCO defined in children nodes.
+ */
+ private function findNodeParams(Sco $sco, \DOMNode $item)
+ {
+ while (!is_null($item)) {
+ switch ($item->nodeName) {
+ case 'title':
+ $sco->setTitle($item->nodeValue);
+ break;
+ case 'adlcp:masteryscore':
+ $sco->setScoreToPassInt($item->nodeValue);
+ break;
+ case 'adlcp:maxtimeallowed':
+ case 'imsss:attemptAbsoluteDurationLimit':
+ $sco->setMaxTimeAllowed($item->nodeValue);
+ break;
+ case 'adlcp:timelimitaction':
+ case 'adlcp:timeLimitAction':
+ $action = strtolower($item->nodeValue);
+
+ if ('exit,message' === $action
+ || 'exit,no message' === $action
+ || 'continue,message' === $action
+ || 'continue,no message' === $action) {
+ $sco->setTimeLimitAction($action);
+ }
+ break;
+ case 'adlcp:datafromlms':
+ case 'adlcp:dataFromLMS':
+ $sco->setLaunchData($item->nodeValue);
+ break;
+ case 'adlcp:prerequisites':
+ $sco->setPrerequisites($item->nodeValue);
+ break;
+ case 'imsss:minNormalizedMeasure':
+ $sco->setScoreToPassDecimal($item->nodeValue);
+ break;
+ case 'adlcp:completionThreshold':
+ if ($item->nodeValue && !is_nan($item->nodeValue)) {
+ $sco->setCompletionThreshold(floatval($item->nodeValue));
+ }
+ break;
+ }
+ $item = $item->nextSibling;
+ }
+ }
+
+ /**
+ * Searches for the resource with the given id and retrieve URL to its content.
+ *
+ * @return string URL to the resource associated to the SCO
+ *
+ * @throws InvalidScormArchiveException
+ */
+ public function findEntryUrl($identifierref, \DOMNodeList $resources)
+ {
+ foreach ($resources as $resource) {
+ $identifier = $resource->attributes->getNamedItem('identifier');
+
+ if (!is_null($identifier)) {
+ $identifierValue = $identifier->nodeValue;
+
+ if ($identifierValue === $identifierref) {
+ $href = $resource->attributes->getNamedItem('href');
+
+ if (is_null($href)) {
+ throw new InvalidScormArchiveException('sco_resource_without_href_message');
+ }
+
+ return $href->nodeValue;
+ }
+ }
+ }
+ throw new InvalidScormArchiveException('sco_without_resource_message');
+ }
+}
diff --git a/src/Manager/ScormManager.php b/src/Manager/ScormManager.php
new file mode 100644
index 0000000..1dfbc05
--- /dev/null
+++ b/src/Manager/ScormManager.php
@@ -0,0 +1,587 @@
+<?php
+
+
+namespace Peopleaps\Scorm\Manager;
+
+use App\Models\User;
+use Carbon\Carbon;
+use DOMDocument;
+use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Facades\File;
+use Illuminate\Support\Facades\Storage;
+use League\Flysystem\FileNotFoundException;
+use Peopleaps\Scorm\Entity\Sco;
+use Peopleaps\Scorm\Entity\Scorm;
+use Peopleaps\Scorm\Entity\ScoTracking;
+use Peopleaps\Scorm\Exception\InvalidScormArchiveException;
+use Peopleaps\Scorm\Exception\StorageNotFoundException;
+use Peopleaps\Scorm\Library\ScormLib;
+use Peopleaps\Scorm\Model\ScormModel;
+use Peopleaps\Scorm\Model\ScormScoModel;
+use Peopleaps\Scorm\Model\ScormScoTrackingModel;
+use Ramsey\Uuid\Uuid;
+use ZipArchive;
+
+class ScormManager
+{
+ /** @var ScormLib */
+ private $scormLib;
+
+ /**
+ * Constructor.
+ *
+ * @param string $filesDir
+ * @param string $uploadDir
+ */
+ public function __construct(
+ ) {
+ $this->scormLib = new ScormLib();
+ }
+
+ public function uploadScormArchive(UploadedFile $file)
+ {
+ // Checks if it is a valid scorm archive
+ $scormData = null;
+ $zip = new ZipArchive();
+ $openValue = $zip->open($file);
+
+ $isScormArchive = (true === $openValue) && $zip->getStream('imsmanifest.xml');
+
+ $zip->close();
+
+ if (!$isScormArchive) {
+ throw new InvalidScormArchiveException('invalid_scorm_archive_message');
+ } else {
+ $scormData = $this->generateScorm($file);
+ }
+
+ // save to db
+ if ($scormData && is_array($scormData)) {
+ $scorm = new ScormModel();
+ $scorm->version = $scormData['version'];
+ $scorm->hash_name = $scormData['hashName'];
+ $scorm->origin_file = $scormData['name'];
+ $scorm->origin_file_mime = $scormData['type'];
+ $scorm->uuid = $scormData['hashName'];
+ $scorm->save();
+
+ if (!empty($scormData['scos']) && is_array($scormData['scos'])) {
+ foreach ($scormData['scos'] as $scoData) {
+
+ $scoParent = null;
+ if (!empty($scoData->scoParent)) {
+ $scoParent = ScormScoModel::where('uuid', $scoData->scoParent->uuid)->first();
+ }
+
+ $sco = new ScormScoModel();
+ $sco->scorm_id = $scorm->id;
+ $sco->uuid = $scoData->uuid;
+ $sco->sco_parent_id = $scoParent ? $scoParent->id : null;
+ $sco->entry_url = $scoData->entryUrl;
+ $sco->identifier = $scoData->identifier;
+ $sco->title = $scoData->title;
+ $sco->visible = $scoData->visible;
+ $sco->sco_parameters = $scoData->parameters;
+ $sco->launch_data = $scoData->launchData;
+ $sco->max_time_allowed = $scoData->maxTimeAllowed;
+ $sco->time_limit_action = $scoData->timeLimitAction;
+ $sco->block = $scoData->block;
+ $sco->score_int = $scoData->scoreToPassInt;
+ $sco->score_decimal = $scoData->scoreToPassDecimal;
+ $sco->completion_threshold = $scoData->completionThreshold;
+ $sco->prerequisites = $scoData->prerequisites;
+ $sco->save();
+ }
+ }
+ }
+
+ return $scormData;
+ }
+
+ private function parseScormArchive(UploadedFile $file)
+ {
+ $data = [];
+ $contents = '';
+ $zip = new \ZipArchive();
+
+ $zip->open($file);
+ $stream = $zip->getStream('imsmanifest.xml');
+
+ while (!feof($stream)) {
+ $contents .= fread($stream, 2);
+ }
+
+
+ $dom = new DOMDocument();
+
+ if (!$dom->loadXML($contents)) {
+ throw new InvalidScormArchiveException('cannot_load_imsmanifest_message');
+ }
+
+ $scormVersionElements = $dom->getElementsByTagName('schemaversion');
+
+ if (1 === $scormVersionElements->length) {
+ switch ($scormVersionElements->item(0)->textContent) {
+ case '1.2':
+ $data['version'] = Scorm::SCORM_12;
+ break;
+ case 'CAM 1.3':
+ case '2004 3rd Edition':
+ case '2004 4th Edition':
+ $data['version'] = Scorm::SCORM_2004;
+ break;
+ default:
+ throw new InvalidScormArchiveException('invalid_scorm_version_message');
+ }
+ } else {
+ throw new InvalidScormArchiveException('invalid_scorm_version_message');
+ }
+ $scos = $this->scormLib->parseOrganizationsNode($dom);
+
+ if (0 >= count($scos)) {
+ throw new InvalidScormArchiveException('no_sco_in_scorm_archive_message');
+ }
+ $data['scos'] = $scos;
+
+ return $data;
+ }
+
+ /**
+ * Unzip a given ZIP file into the web resources directory.
+ *
+ * @param string $hashName name of the destination directory
+ */
+ private function unzipScormArchive(UploadedFile $file, $hashName)
+ {
+ $zip = new \ZipArchive();
+ $zip->open($file);
+
+ if (!config()->has('filesystems.disks.'.config('scorm.disk').'.root')) {
+ throw new StorageNotFoundException();
+ }
+
+ $rootFolder = config('filesystems.disks.'.config('scorm.disk').'.root').'/';
+ $destinationDir = config('scorm.upload_path').$hashName; // file path
+
+ if (!File::isDirectory($rootFolder.$destinationDir)) {
+ File::makeDirectory($rootFolder.$destinationDir, 0755, true, true);
+ }
+
+ $zip->extractTo($rootFolder.$destinationDir);
+ $zip->close();
+ }
+
+ /**
+ * @param UploadedFile $file
+ * @return array
+ * @throws InvalidScormArchiveException
+ */
+ private function generateScorm(UploadedFile $file)
+ {
+ $hashName = Uuid::uuid4();
+ $hashFileName = $hashName.'.zip';
+ $scormData = $this->parseScormArchive($file);
+ $this->unzipScormArchive($file, $hashName);
+
+ if (!config()->has('filesystems.disks.'.config('scorm.disk').'.root')) {
+ throw new StorageNotFoundException();
+ }
+
+ $rootFolder = config('filesystems.disks.'.config('scorm.disk').'.root').'/';
+ $destinationDir = config('scorm.upload_path').$hashName; // file path
+
+ // Move Scorm archive in the files directory
+ $finalFile = $file->move($rootFolder.$destinationDir, $hashName.'.zip');
+
+ return [
+ 'name' => $hashFileName, // to follow standard file data format
+ 'hashName' => $hashName,
+ 'type' => $finalFile->getMimeType(),
+ 'version' => $scormData['version'],
+ 'scos' => $scormData['scos'],
+ ];
+ }
+
+ public function createScoTracking($scoUuid, $userId = null)
+ {
+ $sco = ScormScoModel::where('uuid', $scoUuid)->firstOrFail();
+
+ $version = $sco->scorm->version;
+ $scoTracking = new ScoTracking();
+ $scoTracking->setSco($sco->toArray());
+
+ switch ($version) {
+ case Scorm::SCORM_12:
+ $scoTracking->setLessonStatus('not attempted');
+ $scoTracking->setSuspendData('');
+ $scoTracking->setEntry('ab-initio');
+ $scoTracking->setLessonLocation('');
+ $scoTracking->setCredit('no-credit');
+ $scoTracking->setTotalTimeInt(0);
+ $scoTracking->setSessionTime(0);
+ $scoTracking->setLessonMode('normal');
+ $scoTracking->setExitMode('');
+
+ if (is_null($sco->prerequisites)) {
+ $scoTracking->setIsLocked(false);
+ } else {
+ $scoTracking->setIsLocked(true);
+ }
+ break;
+ case Scorm::SCORM_2004:
+ $scoTracking->setTotalTimeString('PT0S');
+ $scoTracking->setCompletionStatus('unknown');
+ $scoTracking->setLessonStatus('unknown');
+ $scoTracking->setIsLocked(false);
+ break;
+ }
+
+ $scoTracking->setUserId($userId);
+
+ // Create a new tracking model
+ $storeTracking = ScormScoTrackingModel::firstOrCreate([
+ 'user_id' => $userId,
+ 'sco_id' => $sco->id
+ ], [
+ 'uuid' => Uuid::uuid4(),
+ 'progression' => $scoTracking->getProgression(),
+ 'score_raw' => $scoTracking->getScoreRaw(),
+ 'score_min' => $scoTracking->getScoreMin(),
+ 'score_max' => $scoTracking->getScoreMax(),
+ 'score_scaled' => $scoTracking->getScoreScaled(),
+ 'lesson_status' => $scoTracking->getLessonStatus(),
+ 'completion_status' => $scoTracking->getCompletionStatus(),
+ 'session_time' => $scoTracking->getSessionTime(),
+ 'total_time_int' => $scoTracking->getTotalTimeInt(),
+ 'total_time_string' => $scoTracking->getTotalTimeString(),
+ 'entry' => $scoTracking->getEntry(),
+ 'suspend_data' => $scoTracking->getSuspendData(),
+ 'credit' => $scoTracking->getCredit(),
+ 'exit_mode' => $scoTracking->getExitMode(),
+ 'lesson_location' => $scoTracking->getLessonLocation(),
+ 'lesson_mode' => $scoTracking->getLessonMode(),
+ 'is_locked' => $scoTracking->getIsLocked(),
+ 'details' => $scoTracking->getDetails(),
+ 'latest_date' => $scoTracking->getLatestDate(),
+ 'created_at' => Carbon::now(),
+ 'updated_at' => Carbon::now(),
+ ]);
+
+ $scoTracking->setProgression($storeTracking->progression);
+ $scoTracking->setScoreRaw($storeTracking->score_raw);
+ $scoTracking->setScoreMin($storeTracking->score_min);
+ $scoTracking->setScoreMax($storeTracking->score_max);
+ $scoTracking->setScoreScaled($storeTracking->score_scaled);
+ $scoTracking->setLessonStatus($storeTracking->lesson_status);
+ $scoTracking->setCompletionStatus($storeTracking->completion_status);
+ $scoTracking->setSessionTime($storeTracking->session_time);
+ $scoTracking->setTotalTimeInt($storeTracking->total_time_int);
+ $scoTracking->setTotalTimeString($storeTracking->total_time_string);
+ $scoTracking->setEntry($storeTracking->entry);
+ $scoTracking->setSuspendData($storeTracking->suspend_data);
+ $scoTracking->setCredit($storeTracking->credit);
+ $scoTracking->setExitMode($storeTracking->exit_mode);
+ $scoTracking->setLessonLocation($storeTracking->lesson_location);
+ $scoTracking->setLessonMode($storeTracking->lesson_mode);
+ $scoTracking->setIsLocked($storeTracking->is_locked);
+ $scoTracking->setDetails($storeTracking->details);
+ $scoTracking->setLatestDate($storeTracking->latest_date);
+
+ return $storeTracking;
+ }
+
+ public function updateScoTracking($scoUuid, $userId, $data)
+ {
+ $tracking = $this->createScoTracking($scoUuid, $userId);
+ $tracking->setLatestDate(new \DateTime());
+ $sco = $tracking->getSco();
+ $scorm = ScormModel::where('id', $sco['id'])->firstOrFail();
+
+ $statusPriority = [
+ 'unknown' => 0,
+ 'not attempted' => 1,
+ 'browsed' => 2,
+ 'incomplete' => 3,
+ 'completed' => 4,
+ 'failed' => 5,
+ 'passed' => 6,
+ ];
+
+ switch ($scorm->version) {
+ case Scorm::SCORM_12:
+ $scoreRaw = isset($data['cmi.core.score.raw']) ? intval($data['cmi.core.score.raw']) : null;
+ $scoreMin = isset($data['cmi.core.score.min']) ? intval($data['cmi.core.score.min']) : null;
+ $scoreMax = isset($data['cmi.core.score.max']) ? intval($data['cmi.core.score.max']) : null;
+ $lessonStatus = isset($data['cmi.core.lesson_status']) ? $data['cmi.core.lesson_status'] : null;
+ $sessionTime = isset($data['cmi.core.session_time']) ? $data['cmi.core.session_time'] : null;
+ $sessionTimeInHundredth = $this->convertTimeInHundredth($sessionTime);
+ $progression = isset($data['cmi.progress_measure']) ? floatval($data['cmi.progress_measure']) : 0;
+
+ $tracking->setDetails($data);
+ $tracking->setEntry($data['cmi.core.entry']);
+ $tracking->setExitMode($data['cmi.core.exit']);
+ $tracking->setLessonLocation($data['cmi.core.lesson_location']);
+ $tracking->setSessionTime($sessionTimeInHundredth);
+ $tracking->setSuspendData($data['cmi.suspend_data']);
+
+ // Compute total time
+ $totalTimeInHundredth = $this->convertTimeInHundredth($data['cmi.core.total_time']);
+ $totalTimeInHundredth += $sessionTimeInHundredth;
+
+ // Persist total time
+ $tracking->setTotalTime($totalTimeInHundredth, Scorm::SCORM_12);
+
+ $bestScore = $tracking->getScoreRaw();
+ $bestStatus = $tracking->getLessonStatus();
+
+ // Update best score if the current score is better than the previous best score
+ if (empty($bestScore) || (!is_null($scoreRaw) && $scoreRaw > $bestScore)) {
+ $tracking->setScoreRaw($scoreRaw);
+ $bestScore = $scoreRaw;
+ }
+
+ if (empty($bestStatus) || ($lessonStatus !== $bestStatus && $statusPriority[$lessonStatus] > $statusPriority[$bestStatus])) {
+ $tracking->setLessonStatus($lessonStatus);
+ $bestStatus = $lessonStatus;
+ }
+
+ if (empty($progression) && ('completed' === $bestStatus || 'passed' === $bestStatus)) {
+ $progression = 100;
+ }
+
+ if ($progression > $tracking->getProgression()) {
+ $tracking->setProgression($progression);
+ }
+
+ break;
+
+ case Scorm::SCORM_2004:
+ $tracking->setDetails($data);
+
+ if (isset($data['cmi.suspend_data'])) {
+ $tracking->setSuspendData($data['cmi.suspend_data']);
+ }
+
+ $dataSessionTime = isset($data['cmi.session_time']) ?
+ $this->formatSessionTime($data['cmi.session_time']) :
+ 'PT0S';
+ $completionStatus = isset($data['cmi.completion_status']) ? $data['cmi.completion_status'] : 'unknown';
+ $successStatus = isset($data['cmi.success_status']) ? $data['cmi.success_status'] : 'unknown';
+ $scoreRaw = isset($data['cmi.score.raw']) ? intval($data['cmi.score.raw']) : null;
+ $scoreMin = isset($data['cmi.score.min']) ? intval($data['cmi.score.min']) : null;
+ $scoreMax = isset($data['cmi.score.max']) ? intval($data['cmi.score.max']) : null;
+ $scoreScaled = isset($data['cmi.score.scaled']) ? floatval($data['cmi.score.scaled']) : null;
+ $progression = isset($data['cmi.progress_measure']) ? floatval($data['cmi.progress_measure']) : 0;
+ $bestScore = $tracking->getScoreRaw();
+
+ // Computes total time
+ $totalTime = new \DateInterval($tracking->getTotalTimeString());
+
+ try {
+ $sessionTime = new \DateInterval($dataSessionTime);
+ } catch (\Exception $e) {
+ $sessionTime = new \DateInterval('PT0S');
+ }
+ $computedTime = new \DateTime();
+ $computedTime->setTimestamp(0);
+ $computedTime->add($totalTime);
+ $computedTime->add($sessionTime);
+ $computedTimeInSecond = $computedTime->getTimestamp();
+ $totalTimeInterval = $this->retrieveIntervalFromSeconds($computedTimeInSecond);
+ $data['cmi.total_time'] = $totalTimeInterval;
+ $tracking->setTotalTimeString($totalTimeInterval);
+
+ // Update best score if the current score is better than the previous best score
+ if (empty($bestScore) || (!is_null($scoreRaw) && $scoreRaw > $bestScore)) {
+ $tracking->setScoreRaw($scoreRaw);
+ $tracking->setScoreMin($scoreMin);
+ $tracking->setScoreMax($scoreMax);
+ $tracking->setScoreScaled($scoreScaled);
+ }
+
+ // Update best success status and completion status
+ $lessonStatus = $completionStatus;
+ if (in_array($successStatus, ['passed', 'failed'])) {
+ $lessonStatus = $successStatus;
+ }
+
+ $bestStatus = $tracking->getLessonStatus();
+ if (empty($bestStatus) || ($lessonStatus !== $bestStatus && $statusPriority[$lessonStatus] > $statusPriority[$bestStatus])) {
+ $tracking->setLessonStatus($lessonStatus);
+ $bestStatus = $lessonStatus;
+ }
+
+ if (empty($tracking->getCompletionStatus())
+ || ($completionStatus !== $tracking->getCompletionStatus() && $statusPriority[$completionStatus] > $statusPriority[$tracking->getCompletionStatus()])
+ ) {
+ // This is no longer needed as completionStatus and successStatus are merged together
+ // I keep it for now for possible retro compatibility
+ $tracking->setCompletionStatus($completionStatus);
+ }
+
+ if (empty($progression) && ('completed' === $bestStatus || 'passed' === $bestStatus)) {
+ $progression = 100;
+ }
+
+ if ($progression > $tracking->getProgression()) {
+ $tracking->setProgression($progression);
+ }
+
+ break;
+ }
+
+ $updateResult = ScormScoTrackingModel::where('user_id', $tracking->getUserId())
+ ->where('sco_id', $sco['id'])
+ ->firstOrFail();
+
+ if ($tracking->getProgression()) {
+ $updateResult->progression = $tracking->getProgression();
+ }
+
+ if ($tracking->getScoreRaw()) {
+ $updateResult->score_raw = $tracking->getScoreRaw();
+ }
+
+ if ($tracking->getScoreMin()) {
+ $updateResult->score_min = $tracking->getScoreMin();
+ }
+
+ if ($tracking->getScoreMax()) {
+ $updateResult->score_max = $tracking->getScoreMax();
+ }
+
+ if ($tracking->getScoreScaled()) {
+ $updateResult->score_scaled = $tracking->getScoreScaled();
+ }
+
+ if ($tracking->getLessonStatus()) {
+ $updateResult->lesson_status = $tracking->getLessonStatus();
+ }
+
+ if ($tracking->getCompletionStatus()) {
+ $updateResult->completion_status = $tracking->getCompletionStatus();
+ }
+
+ if ($tracking->getSessionTime()) {
+ $updateResult->session_time = $tracking->getSessionTime();
+ }
+
+ if ($tracking->getTotalTimeInt()) {
+ $updateResult->total_time_int = $tracking->getTotalTimeInt();
+ }
+
+ if ($tracking->getTotalTimeString()) {
+ $updateResult->total_time_string = $tracking->getTotalTimeString();
+ }
+
+ if ($tracking->getEntry()) {
+ $updateResult->entry = $tracking->getEntry();
+ }
+
+ if ($tracking->getSuspendData()) {
+ $updateResult->suspend_data = $tracking->getSuspendData();
+ }
+
+ if ($tracking->getCredit()) {
+ $updateResult->credit = $tracking->getCredit();
+ }
+
+ if ($tracking->getExitMode()) {
+ $updateResult->exit_mode = $tracking->getExitMode();
+ }
+
+ if ($tracking->getLessonLocation()) {
+ $updateResult->lesson_location = $tracking->getLessonLocation();
+ }
+
+ if ($tracking->getLessonMode()) {
+ $updateResult->lesson_mode = $tracking->getLessonMode();
+ }
+
+ if ($tracking->getIsLocked()) {
+ $updateResult->is_locked = $tracking->getIsLocked();
+ }
+
+ if ($tracking->getDetails()) {
+ $updateResult->details = $tracking->getDetails();
+ }
+
+ if ($tracking->getLatestDate()) {
+ $updateResult->latest_date = $tracking->getLatestDate();
+ }
+
+ $updateResult->save();
+
+// $this->om->persist($tracking);
+// $this->om->flush();
+
+ return $updateResult;
+ }
+
+ private function convertTimeInHundredth($time)
+ {
+ if ($time != null) {
+ $timeInArray = explode(':', $time);
+ $timeInArraySec = explode('.', $timeInArray[2]);
+ $timeInHundredth = 0;
+
+ if (isset($timeInArraySec[1])) {
+ if (1 === strlen($timeInArraySec[1])) {
+ $timeInArraySec[1] .= '0';
+ }
+ $timeInHundredth = intval($timeInArraySec[1]);
+ }
+ $timeInHundredth += intval($timeInArraySec[0]) * 100;
+ $timeInHundredth += intval($timeInArray[1]) * 6000;
+ $timeInHundredth += intval($timeInArray[0]) * 360000;
+
+ return $timeInHundredth;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Converts a time in seconds to a DateInterval string.
+ *
+ * @param int $seconds
+ *
+ * @return string
+ */
+ private function retrieveIntervalFromSeconds($seconds)
+ {
+ $result = '';
+ $remainingTime = (int) $seconds;
+
+ if (empty($remainingTime)) {
+ $result .= 'PT0S';
+ } else {
+ $nbDays = (int) ($remainingTime / 86400);
+ $remainingTime %= 86400;
+ $nbHours = (int) ($remainingTime / 3600);
+ $remainingTime %= 3600;
+ $nbMinutes = (int) ($remainingTime / 60);
+ $nbSeconds = $remainingTime % 60;
+ $result .= 'P'.$nbDays.'DT'.$nbHours.'H'.$nbMinutes.'M'.$nbSeconds.'S';
+ }
+
+ return $result;
+ }
+
+ private function formatSessionTime($sessionTime)
+ {
+ $formattedValue = 'PT0S';
+ $generalPattern = '/^P([0-9]+Y)?([0-9]+M)?([0-9]+D)?T([0-9]+H)?([0-9]+M)?([0-9]+S)?$/';
+ $decimalPattern = '/^P([0-9]+Y)?([0-9]+M)?([0-9]+D)?T([0-9]+H)?([0-9]+M)?[0-9]+\.[0-9]{1,2}S$/';
+
+ if ('PT' !== $sessionTime) {
+ if (preg_match($generalPattern, $sessionTime)) {
+ $formattedValue = $sessionTime;
+ } elseif (preg_match($decimalPattern, $sessionTime)) {
+ $formattedValue = preg_replace(['/\.[0-9]+S$/'], ['S'], $sessionTime);
+ }
+ }
+
+ return $formattedValue;
+ }
+}
diff --git a/src/Model/ScormModel.php b/src/Model/ScormModel.php
new file mode 100644
index 0000000..d5c8499
--- /dev/null
+++ b/src/Model/ScormModel.php
@@ -0,0 +1,19 @@
+<?php
+
+
+namespace Peopleaps\Scorm\Model;
+
+
+use Illuminate\Database\Eloquent\Model;
+
+class ScormModel extends Model
+{
+ public function getTable()
+ {
+ return config('scorm.table_names.scorm_table', parent::getTable());
+ }
+
+ public function scos() {
+ return $this->hasMany(ScormScoModel::class, 'scorm_id', 'id');
+ }
+}
diff --git a/src/Model/ScormScoModel.php b/src/Model/ScormScoModel.php
new file mode 100644
index 0000000..6577514
--- /dev/null
+++ b/src/Model/ScormScoModel.php
@@ -0,0 +1,19 @@
+<?php
+
+
+namespace Peopleaps\Scorm\Model;
+
+
+use Illuminate\Database\Eloquent\Model;
+
+class ScormScoModel extends Model
+{
+ public function getTable()
+ {
+ return config('scorm.table_names.scorm_sco_table', parent::getTable());
+ }
+
+ public function scorm() {
+ return $this->belongsTo(ScormModel::class, 'scorm_id', 'id');
+ }
+}
diff --git a/src/Model/ScormScoTrackingModel.php b/src/Model/ScormScoTrackingModel.php
new file mode 100644
index 0000000..1a26efb
--- /dev/null
+++ b/src/Model/ScormScoTrackingModel.php
@@ -0,0 +1,42 @@
+<?php
+
+
+namespace Peopleaps\Scorm\Model;
+
+
+use Illuminate\Database\Eloquent\Model;
+
+class ScormScoTrackingModel extends Model
+{
+ protected $fillable = [
+ 'user_id',
+ 'sco_id',
+ 'uuid',
+ 'progression',
+ 'score_raw',
+ 'score_min',
+ 'score_max',
+ 'score_scaled',
+ 'lesson_status',
+ 'completion_status',
+ 'session_time',
+ 'total_time_int',
+ 'total_time_string',
+ 'entry',
+ 'suspend_data',
+ 'credit',
+ 'exit_mode',
+ 'lesson_location',
+ 'lesson_mode',
+ 'is_locked',
+ 'details',
+ 'latest_date',
+ 'created_at',
+ 'updated_at'
+ ];
+
+ public function getTable()
+ {
+ return config('scorm.table_names.scorm_sco_tracking_table', parent::getTable());
+ }
+}
diff --git a/src/ScormServiceProvider.php b/src/ScormServiceProvider.php
new file mode 100644
index 0000000..1806775
--- /dev/null
+++ b/src/ScormServiceProvider.php
@@ -0,0 +1,62 @@
+<?php
+
+
+namespace Peopleaps\Scorm;
+
+
+use Illuminate\Filesystem\Filesystem;
+use Illuminate\Support\Collection;
+use Illuminate\Support\ServiceProvider;
+use Peopleaps\Scorm\Manager\ScormManager;
+
+class ScormServiceProvider extends ServiceProvider
+{
+ public function register()
+ {
+ $this->app->bind('scorm-manager', function($app) {
+ return new ScormManager();
+ });
+ }
+
+ public function boot()
+ {
+ $this->offerPublishing();
+ }
+
+ protected function offerPublishing() {
+ // function not available and 'publish' not relevant in Lumen
+ if (! function_exists('config_path')) {
+ return;
+ }
+
+ $this->publishes([
+ __DIR__.'/../config/scorm.php' => config_path('scorm.php'),
+ ], 'config');
+
+ $this->publishes([
+ __DIR__.'/../database/migrations/create_scorm_tables.php.stub' => $this->getMigrationFileName('create_scorm_tables.php'),
+ ], 'migrations');
+ }
+
+ /**
+ * Returns existing migration file if found, else uses the current timestamp.
+ *
+ * @return string
+ */
+ protected function getMigrationFileName($migrationFileName): string
+ {
+ $timestamp = date('Y_m_d_His');
+
+ $filesystem = $this->app->make(Filesystem::class);
+
+ return Collection::make($this->app->databasePath().DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR)
+ ->flatMap(function ($path) use ($filesystem) {
+ return $filesystem->glob($path.'*_create_scorm_tables.php');
+ })->push($this->app->databasePath()."/migrations/{$timestamp}_create_scorm_tables.php")
+ ->flatMap(function ($path) use ($filesystem, $migrationFileName) {
+ return $filesystem->glob($path.'*_'.$migrationFileName);
+ })
+ ->push($this->app->databasePath()."/migrations/{$timestamp}_{$migrationFileName}")
+ ->first();
+ }
+}