*/ class StorageManager extends ConfigurableService { const SERVICE_ID = 'taoQtiTest/StorageManager'; /** * The data does not exist in the storage */ const STATE_NOT_FOUND = -1; /** * The data is aligned with the storage */ const STATE_ALIGNED = 0; /** * The data is pending write to the storage */ const STATE_PENDING_WRITE = 1; /** * The data is pending delete from the storage */ const STATE_PENDING_DELETE = 2; /** * Link to the actual storage adapter * @var StateStorage */ protected $storage; /** * In memory cache for read/pending data * @var array */ protected $cache = []; /** * Gets a key that will be used to cache data. * * @param string $userId * @param string $callId * @return string */ protected function getCacheKey($userId, $callId) { return $userId . '/' . $callId; } /** * Puts data in the cache. Maintain the link to the userId/callId pair. * Also keep the dirty state that will be used when persisting the data to the actual storage. * * @param string $key * @param string $userId * @param string $callId * @param string $data * @param int $state */ protected function putInCache($key, $userId, $callId, $data, $state = self::STATE_ALIGNED) { $this->cache[$key] = [ 'userId' => $userId, 'callId' => $callId, 'state' => $state, 'data' => $data ]; } /** * Checks if a dataset exists for the provided key. * * @param string $key * @return bool */ protected function exists($key) { return isset($this->cache[$key]) && in_array($this->cache[$key]['state'], [self::STATE_ALIGNED, self::STATE_PENDING_WRITE]); } /** * Gets a dataset from the cache. * * @param string $key * @return mixed */ protected function getFromCache($key) { if ($this->exists($key)) { return $this->cache[$key]['data']; } return null; } /** * Persists a cache entry and update its status. * * @param string $key * @return bool */ protected function persistCacheEntry($key) { $success = true; if (isset($this->cache[$key])) { $cache = $this->cache[$key]; switch ($cache['state']) { case self::STATE_PENDING_WRITE: $success = $this->getStorage()->set($cache['userId'], $cache['callId'], $cache['data']); if (!$success) { throw new \common_exception_Error('Can\'t write into test runner state storage at ' . static::class); } $this->cache[$key]['state'] = self::STATE_ALIGNED; break; case self::STATE_PENDING_DELETE: $success = $this->getStorage()->del($cache['userId'], $cache['callId']); if ($success) { unset($this->cache[$key]); } break; } } return $success; } /** * @return StateStorage */ public function getStorage() { if (!$this->storage) { $this->storage = $this->getServiceLocator()->get(StateStorage::SERVICE_ID); } return $this->storage; } /** * @param StateStorage $storage * @return StorageManager */ public function setStorage(StateStorage $storage) { $this->storage = $storage; return $this; } /** * Applies a dataset to be stored. * * @param string $userId * @param string $callId * @param string $data * @return boolean */ public function set($userId, $callId, $data) { $key = $this->getCacheKey($userId, $callId); $cache = $this->getFromCache($key); if (is_null($cache) || $cache != $data) { $this->putInCache($key, $userId, $callId, $data, self::STATE_PENDING_WRITE); } return true; } /** * Gets a dataset from the store using the provided keys. * Will return null if the dataset doesn't exist. * * @param string $userId * @param string $callId * @return string */ public function get($userId, $callId) { $key = $this->getCacheKey($userId, $callId); if (!isset($this->cache[$key])) { $data = $this->getStorage()->get($userId, $callId); $state = is_null($data) ? self::STATE_NOT_FOUND : self::STATE_ALIGNED; $this->putInCache($key, $userId, $callId, $data, $state); } return $this->getFromCache($key); } /** * Whenever or not a dataset exists. * * @param string $userId * @param string $callId * @return boolean */ public function has($userId, $callId) { $key = $this->getCacheKey($userId, $callId); if (!isset($this->cache[$key])) { return $this->getStorage()->has($userId, $callId); } return $this->exists($key); } /** * Marks the the dataset to be removed from the storage. * * @param string $userId * @param string $callId * @return boolean */ public function del($userId, $callId) { $key = $this->getCacheKey($userId, $callId); $this->putInCache($key, $userId, $callId, null, self::STATE_PENDING_DELETE); return true; } /** * Sends the changes to the storage. * * @param string $userId * @param string $callId * @return bool */ public function persist($userId = null, $callId = null) { if ($userId && $callId) { $keys = [$this->getCacheKey($userId, $callId)]; } else { $keys = array_keys($this->cache); } $success = true; foreach ($keys as $key) { if (!$this->persistCacheEntry($key)) { $success = false; } } return $success; } }