diff --git a/core/class/autoload.php b/core/class/autoload.php index d201b2f8..ac507c05 100644 --- a/core/class/autoload.php +++ b/core/class/autoload.php @@ -12,6 +12,5 @@ class autoload { require_once 'core/class/phpmailer/SMTP.class.php'; require_once "core/class/jsondb/Dot.class.php"; require_once "core/class/jsondb/JsonDb.class.php"; - require_once "core/class/sleekDB/SleekDB.php"; } } \ No newline at end of file diff --git a/core/class/sleekDB/Cache.php b/core/class/sleekDB/Cache.php deleted file mode 100644 index 806cb2a9..00000000 --- a/core/class/sleekDB/Cache.php +++ /dev/null @@ -1,309 +0,0 @@ -setCacheDir($cacheDir); - - $this->setCachePath($storePath); - - $this->setTokenArray($cacheTokenArray); - - $this->lifetime = $cacheLifetime; - } - - /** - * Retrieve the cache lifetime for current query. - * @return int|null lifetime in seconds (int) or no lifetime with null - */ - public function getLifetime() - { - return $this->lifetime; - } - - /** - * Retrieve the path to cache folder of current store. - * @return string path to cache directory - */ - public function getCachePath(): string - { - return $this->cachePath; - } - - /** - * Retrieve the cache token used as filename to store cache file. - * @return string unique token for current query. - */ - public function getToken(): string - { - $tokenArray = $this->getTokenArray(); - $tokenArray = self::convertClosuresToString($tokenArray); - - return md5(json_encode($tokenArray)); - } - - /** - * Delete all cache files for current store. - * @return bool - */ - public function deleteAll(): bool - { - return IoHelper::deleteFiles(glob($this->getCachePath()."*")); - } - - /** - * Delete all cache files with no lifetime (null) in current store. - * @return bool - */ - public function deleteAllWithNoLifetime(): bool - { - $noLifetimeFileString = self::NO_LIFETIME_FILE_STRING; - return IoHelper::deleteFiles(glob($this->getCachePath()."*.$noLifetimeFileString.json")); - } - - /** - * Save content for current query as a cache file. - * @param array $content - * @throws IOException if cache folder is not writable or saving failed. - */ - public function set(array $content){ - $lifetime = $this->getLifetime(); - $cachePath = $this->getCachePath(); - $token = $this->getToken(); - - $noLifetimeFileString = self::NO_LIFETIME_FILE_STRING; - $cacheFile = $cachePath . $token . ".$noLifetimeFileString.json"; - - if(is_int($lifetime)){ - $cacheFile = $cachePath . $token . ".$lifetime.json"; - } - - IoHelper::writeContentToFile($cacheFile, json_encode($content)); - } - - /** - * Retrieve content of cache file. - * @return array|null array on success, else null - * @throws IOException if cache file is not readable or does not exist. - */ - public function get(){ - $cachePath = $this->getCachePath(); - $token = $this->getToken(); - - $cacheFile = null; - - IoHelper::checkRead($cachePath); - - $cacheFiles = glob($cachePath.$token."*.json"); - - if($cacheFiles !== false && count($cacheFiles) > 0){ - $cacheFile = $cacheFiles[0]; - } - - if(!empty($cacheFile)){ - $cacheParts = explode(".", $cacheFile); - if(count($cacheParts) >= 3){ - $lifetime = $cacheParts[count($cacheParts) - 2]; - if(is_numeric($lifetime)){ - if($lifetime === "0"){ - return json_decode(IoHelper::getFileContent($cacheFile), true); - } - $fileExpiredAfter = filemtime($cacheFile) + (int) $lifetime; - if(time() <= $fileExpiredAfter){ - return json_decode(IoHelper::getFileContent($cacheFile), true); - } - IoHelper::deleteFile($cacheFile); - } else if($lifetime === self::NO_LIFETIME_FILE_STRING){ - return json_decode(IoHelper::getFileContent($cacheFile), true); - } - } - } - return null; - } - - /** - * Delete cache file/s for current query. - * @return bool - */ - public function delete(): bool - { - return IoHelper::deleteFiles(glob($this->getCachePath().$this->getToken()."*.json")); - } - - /** - * @param string $storePath - * @return Cache - */ - private function setCachePath(string $storePath): Cache - { - $cachePath = ""; - $cacheDir = $this->getCacheDir(); - - if(!empty($storePath)){ - IoHelper::normalizeDirectory($storePath); - $cachePath = $storePath . $cacheDir; - } - - $this->cachePath = $cachePath; - - return $this; - } - - /** - * Set the cache token array used for cache token string generation. - * @param array $tokenArray - * @return Cache - */ - private function setTokenArray(array &$tokenArray): Cache - { - $this->tokenArray = &$tokenArray; - return $this; - } - - /** - * Retrieve the cache token array. - * @return array - */ - private function getTokenArray(): array - { - return $this->tokenArray; - } - - /** - * Convert one or multiple closures to string. If array provided, recursively. - * @param mixed $data - * @return mixed - */ - private static function convertClosuresToString($data){ - if(!is_array($data)){ - if($data instanceof \Closure){ - return self::getClosureAsString($data); - } - return $data; - } - foreach ($data as $key => $token){ - if(is_array($token)){ - $data[$key] = self::convertClosuresToString($token); - } else if($token instanceof \Closure){ - $data[$key] = self::getClosureAsString($token); - } - } - return $data; - } - - /** - * Retrieve a string representation of a closure that can be used to differentiate between closures - * when generating the cache token string. - * @param Closure $closure - * @return false|string string representation of closure or false on failure. - */ - private static function getClosureAsString(Closure $closure) - { - try{ - $reflectionFunction = new ReflectionFunction($closure); // get reflection object - } catch (Exception $exception){ - return false; - } - $filePath = $reflectionFunction->getFileName(); // absolute path of php file containing function - $startLine = $reflectionFunction->getStartLine(); // start line of function - $endLine = $reflectionFunction->getEndLine(); // end line of function - $lineSeparator = PHP_EOL; // line separator "\n" - - $staticVariables = $reflectionFunction->getStaticVariables(); - $staticVariables = var_export($staticVariables, true); - - if($filePath === false || $startLine === false || $endLine === false){ - return false; - } - - $startEndDifference = $endLine - $startLine; - - $startLine--; // -1 to use it with the array representation of the file - - if($startLine < 0 || $startEndDifference < 0){ - return false; - } - - // get content of file containing function - $fp = fopen($filePath, 'rb'); - $fileContent = ""; - if(flock($fp, LOCK_SH)){ - $fileContent = @stream_get_contents($fp); - } - flock($fp, LOCK_UN); - fclose($fp); - - if(empty($fileContent)){ - return false; - } - - // separate the file into an array containing every line as one element - $fileContentArray = explode($lineSeparator, $fileContent); - if(count($fileContentArray) < $endLine){ - return false; - } - - // return the part of the file containing the function as a string. - $functionString = implode("", array_slice($fileContentArray, $startLine, $startEndDifference + 1)); - $functionString .= "|staticScopeVariables:".$staticVariables; - return $functionString; - } - - /** - * Set the cache directory name. - * @param string $cacheDir - * @return Cache - */ - private function setCacheDir(string $cacheDir): Cache - { - IoHelper::normalizeDirectory($cacheDir); - $this->cacheDir = $cacheDir; - return $this; - } - - /** - * Retrieve the cache directory name or the default cache directory name if empty. - * @return string - */ - private function getCacheDir(): string - { - return (!empty($this->cacheDir)) ? $this->cacheDir : self::DEFAULT_CACHE_DIR; - } -} \ No newline at end of file diff --git a/core/class/sleekDB/Classes/CacheHandler.php b/core/class/sleekDB/Classes/CacheHandler.php deleted file mode 100644 index 2104d9f8..00000000 --- a/core/class/sleekDB/Classes/CacheHandler.php +++ /dev/null @@ -1,129 +0,0 @@ -cacheTokenArray = $queryBuilder->_getCacheTokenArray(); - - $queryBuilderProperties = $queryBuilder->_getConditionProperties(); - $this->useCache = $queryBuilderProperties["useCache"]; - $this->regenerateCache = $queryBuilderProperties["regenerateCache"]; - - $this->cache = new Cache($storePath, $this->_getCacheTokenArray(), $queryBuilderProperties["cacheLifetime"]); - } - - /** - * @return Cache - */ - public function getCache(): Cache - { - return $this->cache; - } - - /** - * Get results from cache - * @return array|null - * @throws IOException - */ - public function getCacheContent($getOneDocument) - { - if($this->getUseCache() !== true){ - return null; - } - - $this->updateCacheTokenArray(['oneDocument' => $getOneDocument]); - - if($this->regenerateCache === true) { - $this->getCache()->delete(); - } - - $cacheResults = $this->getCache()->get(); - if(is_array($cacheResults)) { - return $cacheResults; - } - - return null; - } - - /** - * Add content to cache - * @param array $results - * @throws IOException - */ - public function setCacheContent(array $results) - { - if($this->getUseCache() === true){ - $this->getCache()->set($results); - } - } - - /** - * Delete all cache files that have no lifetime. - * @return bool - */ - public function deleteAllWithNoLifetime(): bool - { - return $this->getCache()->deleteAllWithNoLifetime(); - } - - /** - * Returns a reference to the array used for cache token generation - * @return array - */ - public function &_getCacheTokenArray(): array - { - return $this->cacheTokenArray; - } - - /** - * @param array $tokenUpdate - */ - private function updateCacheTokenArray(array $tokenUpdate) - { - if(empty($tokenUpdate)) { - return; - } - $cacheTokenArray = $this->_getCacheTokenArray(); - foreach ($tokenUpdate as $key => $value){ - $cacheTokenArray[$key] = $value; - } - $this->cacheTokenArray = $cacheTokenArray; - } - - /** - * Status if cache is used or not - * @return bool - */ - private function getUseCache(): bool - { - return $this->useCache; - } - -} \ No newline at end of file diff --git a/core/class/sleekDB/Classes/ConditionsHandler.php b/core/class/sleekDB/Classes/ConditionsHandler.php deleted file mode 100644 index 0bb59de6..00000000 --- a/core/class/sleekDB/Classes/ConditionsHandler.php +++ /dev/null @@ -1,433 +0,0 @@ -getTimestamp(); - $fieldValue = self::convertValueToTimeStamp($fieldValue); - } - - $condition = strtolower(trim($condition)); - switch ($condition){ - case "=": - case "===": - return ($fieldValue === $value); - case "==": - return ($fieldValue == $value); - case "<>": - return ($fieldValue != $value); - case "!==": - case "!=": - return ($fieldValue !== $value); - case ">": - return ($fieldValue > $value); - case ">=": - return ($fieldValue >= $value); - case "<": - return ($fieldValue < $value); - case "<=": - return ($fieldValue <= $value); - case "not like": - case "like": - - if(!is_string($value)){ - throw new InvalidArgumentException("When using \"LIKE\" or \"NOT LIKE\" the value has to be a string."); - } - - // escape characters that are part of regular expression syntax - // https://www.php.net/manual/en/function.preg-quote.php - // We can not use preg_quote because the following characters are also wildcard characters in sql - // so we will not escape them: [ ^ ] - - $charactersToEscape = [".", "\\", "+", "*", "?", "$", "(", ")", "{", "}", "=", "!", "<", ">", "|", ":", "#"]; - foreach ($charactersToEscape as $characterToEscape){ - $value = str_replace($characterToEscape, "\\".$characterToEscape, $value); - } - - $value = str_replace(array('%', '_'), array('.*', '.{1}'), $value); // (zero or more characters) and (single character) - $pattern = "/^" . $value . "$/i"; - $result = (preg_match($pattern, $fieldValue) === 1); - return ($condition === "not like") ? !$result : $result; - - case "not in": - case "in": - if(!is_array($value)){ - $value = (!is_object($value) && !is_array($value) && !is_null($value)) ? $value : gettype($value); - throw new InvalidArgumentException("When using \"in\" and \"not in\" you have to check against an array. Got: $value"); - } - if(!empty($value)){ - (list($firstElement) = $value); - if($firstElement instanceof DateTime){ - // if the user wants to use DateTime, every element of the array has to be an DateTime object. - - // compare timestamps - - // null, false or an empty string will convert to current date and time. - // That is not what we want. - if(empty($fieldValue)){ - return false; - } - - foreach ($value as $key => $item){ - if(!($item instanceof DateTime)){ - throw new InvalidArgumentException("If one DateTime object is given in an \"IN\" or \"NOT IN\" comparison, every element has to be a DateTime object!"); - } - $value[$key] = $item->getTimestamp(); - } - - $fieldValue = self::convertValueToTimeStamp($fieldValue); - } - } - $result = in_array($fieldValue, $value, true); - return ($condition === "not in") ? !$result : $result; - case "not between": - case "between": - - if(!is_array($value) || ($valueLength = count($value)) !== 2){ - $value = (!is_object($value) && !is_array($value) && !is_null($value)) ? $value : gettype($value); - if(isset($valueLength)){ - $value .= " | Length: $valueLength"; - } - throw new InvalidArgumentException("When using \"between\" you have to check against an array with a length of 2. Got: $value"); - } - - list($startValue, $endValue) = $value; - - $result = ( - self::verifyCondition(">=", $fieldValue, $startValue) - && self::verifyCondition("<=", $fieldValue, $endValue) - ); - - return ($condition === "not between") ? !$result : $result; - case "not contains": - case "contains": - - if(!is_array($fieldValue)){ - return ($condition === "not contains"); - } - - $fieldValues = []; - - if($value instanceof DateTime){ - // compare timestamps - $value = $value->getTimestamp(); - - foreach ($fieldValue as $item){ - // null, false or an empty string will convert to current date and time. - // That is not what we want. - if(empty($item)){ - continue; - } - try{ - $fieldValues[] = self::convertValueToTimeStamp($item); - } catch (Exception $exception){ - } - } - } - - if(!empty($fieldValues)){ - $result = in_array($value, $fieldValues, true); - } else { - $result = in_array($value, $fieldValue, true); - } - - return ($condition === "not contains") ? !$result : $result; - case 'exists': - return $fieldValue === $value; - default: - throw new InvalidArgumentException("Condition \"$condition\" is not allowed."); - } - } - - /** - * @param array $element condition or operation - * @param array $data - * @return bool - * @throws InvalidArgumentException - */ - public static function handleWhereConditions(array $element, array $data): bool - { - if(empty($element)){ - throw new InvalidArgumentException("Malformed where statement! Where statements can not contain empty arrays."); - } - if(array_keys($element) !== range(0, (count($element) - 1))){ - throw new InvalidArgumentException("Malformed where statement! Associative arrays are not allowed."); - } - // element is a where condition - if(is_string($element[0]) && is_string($element[1])){ - if(count($element) !== 3){ - throw new InvalidArgumentException("Where conditions have to be [fieldName, condition, value]"); - } - - $fieldName = $element[0]; - $condition = strtolower(trim($element[1])); - $fieldValue = ($condition === 'exists') - ? NestedHelper::nestedFieldExists($fieldName, $data) - : NestedHelper::getNestedValue($fieldName, $data); - - return self::verifyCondition($condition, $fieldValue, $element[2]); - } - - // element is an array "brackets" - - // prepare results array - example: [true, "and", false] - $results = []; - foreach ($element as $value){ - if(is_array($value)){ - $results[] = self::handleWhereConditions($value, $data); - } else if (is_string($value)) { - $results[] = $value; - } else if($value instanceof \Closure){ - $result = $value($data); - if(!is_bool($result)){ - $resultType = gettype($result); - $errorMsg = "The closure in the where condition needs to return a boolean. Got: $resultType"; - throw new InvalidArgumentException($errorMsg); - } - $results[] = $result; - } else { - $value = (!is_object($value) && !is_array($value) && !is_null($value)) ? $value : gettype($value); - throw new InvalidArgumentException("Invalid nested where statement element! Expected condition or operation, got: \"$value\""); - } - } - - // first result as default value - $returnValue = array_shift($results); - - if(is_bool($returnValue) === false){ - throw new InvalidArgumentException("Malformed where statement! First part of the statement have to be a condition."); - } - - // used to prioritize the "and" operation. - $orResults = []; - - // use results array to get the return value of the conditions within the bracket - while(!empty($results) || !empty($orResults)){ - - if(empty($results)) { - if($returnValue === true){ - // we need to check anymore, because the result of true || false is true - break; - } - // $orResults is not empty. - $nextResult = array_shift($orResults); - $returnValue = $returnValue || $nextResult; - continue; - } - - $operationOrNextResult = array_shift($results); - - if(is_string($operationOrNextResult)){ - $operation = $operationOrNextResult; - - if(empty($results)){ - throw new InvalidArgumentException("Malformed where statement! Last part of a condition can not be a operation."); - } - $nextResult = array_shift($results); - - if(!is_bool($nextResult)){ - throw new InvalidArgumentException("Malformed where statement! Two operations in a row are not allowed."); - } - } else if(is_bool($operationOrNextResult)){ - $operation = "AND"; - $nextResult = $operationOrNextResult; - } else { - throw new InvalidArgumentException("Malformed where statement! A where statement have to contain just operations and conditions."); - } - - if(!in_array(strtolower($operation), ["and", "or"])){ - $operation = (!is_object($operation) && !is_array($operation) && !is_null($operation)) ? $operation : gettype($operation); - throw new InvalidArgumentException("Expected 'and' or 'or' operator got \"$operation\""); - } - - // prepare $orResults execute after all "and" are done. - if(strtolower($operation) === "or"){ - $orResults[] = $returnValue; - $returnValue = $nextResult; - continue; - } - - $returnValue = $returnValue && $nextResult; - - } - - return $returnValue; - } - - /** - * @param array $results - * @param array $currentDocument - * @param array $distinctFields - * @return bool - */ - public static function handleDistinct(array $results, array $currentDocument, array $distinctFields): bool - { - // Distinct data check. - foreach ($results as $result) { - foreach ($distinctFields as $field) { - try { - $storePassed = (NestedHelper::getNestedValue($field, $result) !== NestedHelper::getNestedValue($field, $currentDocument)); - } catch (Throwable $th) { - continue; - } - if ($storePassed === false) { - return false; - } - } - } - - return true; - } - - /** - * @param array $data - * @param bool $storePassed - * @param array $nestedWhereConditions - * @return bool - * @throws InvalidArgumentException - * @deprecated since version 2.3, use handleWhereConditions instead. - */ - public static function handleNestedWhere(array $data, bool $storePassed, array $nestedWhereConditions): bool - { - // TODO remove nested where with v3.0 - - if(empty($nestedWhereConditions)){ - return $storePassed; - } - - // the outermost operation specify how the given conditions are connected with other conditions, - // like the ones that are specified using the where, orWhere, in or notIn methods - $outerMostOperation = (array_keys($nestedWhereConditions))[0]; - $nestedConditions = $nestedWhereConditions[$outerMostOperation]; - - // specifying outermost is optional and defaults to "and" - $outerMostOperation = (is_string($outerMostOperation)) ? strtolower($outerMostOperation) : "and"; - - // if the document already passed the store with another condition, we dont need to check it. - if($outerMostOperation === "or" && $storePassed === true){ - return true; - } - - return self::_nestedWhereHelper($nestedConditions, $data); - } - - /** - * @param array $element - * @param array $data - * @return bool - * @throws InvalidArgumentException - * @deprecated since version 2.3. use _handleWhere instead - */ - private static function _nestedWhereHelper(array $element, array $data): bool - { - // TODO remove nested where with v3.0 - // element is a where condition - if(array_keys($element) === range(0, (count($element) - 1)) && is_string($element[0])){ - if(count($element) !== 3){ - throw new InvalidArgumentException("Where conditions have to be [fieldName, condition, value]"); - } - - $fieldValue = NestedHelper::getNestedValue($element[0], $data); - - return self::verifyCondition($element[1], $fieldValue, $element[2]); - } - - // element is an array "brackets" - - // prepare results array - example: [true, "and", false] - $results = []; - foreach ($element as $value){ - if(is_array($value)){ - $results[] = self::_nestedWhereHelper($value, $data); - } else if (is_string($value)){ - $results[] = $value; - } else { - $value = (!is_object($value) && !is_array($value)) ? $value : gettype($value); - throw new InvalidArgumentException("Invalid nested where statement element! Expected condition or operation, got: \"$value\""); - } - } - - if(count($results) < 3){ - throw new InvalidArgumentException("Malformed nested where statement! A condition consists of at least 3 elements."); - } - - // first result as default value - $returnValue = array_shift($results); - - // use results array to get the return value of the conditions within the bracket - while(!empty($results)){ - $operation = array_shift($results); - $nextResult = array_shift($results); - - if(((count($results) % 2) !== 0)){ - throw new InvalidArgumentException("Malformed nested where statement!"); - } - - if(!is_string($operation) || !in_array(strtolower($operation), ["and", "or"])){ - $operation = (!is_object($operation) && !is_array($operation)) ? $operation : gettype($operation); - throw new InvalidArgumentException("Expected 'and' or 'or' operator got \"$operation\""); - } - - if(strtolower($operation) === "and"){ - $returnValue = $returnValue && $nextResult; - } else { - $returnValue = $returnValue || $nextResult; - } - } - - return $returnValue; - } - - /** - * @param $value - * @return int - * @throws InvalidArgumentException - */ - private static function convertValueToTimeStamp($value): int - { - $value = (is_string($value)) ? trim($value) : $value; - try{ - return (new DateTime($value))->getTimestamp(); - } catch (Exception $exception){ - $value = (!is_object($value) && !is_array($value)) - ? $value - : gettype($value); - throw new InvalidArgumentException( - "DateTime object given as value to check against. " - . "Could not convert value into DateTime. " - . "Value: $value" - ); - } - } -} \ No newline at end of file diff --git a/core/class/sleekDB/Classes/DocumentFinder.php b/core/class/sleekDB/Classes/DocumentFinder.php deleted file mode 100644 index d2014966..00000000 --- a/core/class/sleekDB/Classes/DocumentFinder.php +++ /dev/null @@ -1,387 +0,0 @@ -storePath = $storePath; - $this->queryBuilderProperties = $queryBuilderProperties; - $this->primaryKey = $primaryKey; - } - - /** - * @param bool $getOneDocument - * @param bool $reduceAndJoinPossible - * @return array - * @throws IOException - * @throws InvalidArgumentException - */ - public function findDocuments(bool $getOneDocument, bool $reduceAndJoinPossible): array - { - $queryBuilderProperties = $this->queryBuilderProperties; - $dataPath = $this->getDataPath(); - $primaryKey = $this->primaryKey; - - $found = []; - // Start collecting and filtering data. - IoHelper::checkRead($dataPath); - - $conditions = $queryBuilderProperties["whereConditions"]; - $distinctFields = $queryBuilderProperties["distinctFields"]; - $nestedWhereConditions = $queryBuilderProperties["nestedWhere"]; - $listOfJoins = $queryBuilderProperties["listOfJoins"]; - $search = $queryBuilderProperties["search"]; - $searchOptions = $queryBuilderProperties["searchOptions"]; - $groupBy = $queryBuilderProperties["groupBy"]; - $havingConditions = $queryBuilderProperties["havingConditions"]; - $fieldsToSelect = $queryBuilderProperties["fieldsToSelect"]; - $orderBy = $queryBuilderProperties["orderBy"]; - $skip = $queryBuilderProperties["skip"]; - $limit = $queryBuilderProperties["limit"]; - $fieldsToExclude = $queryBuilderProperties["fieldsToExclude"]; - - unset($queryBuilderProperties); - - if ($handle = opendir($dataPath)) { - - while (false !== ($entry = readdir($handle))) { - - if ($entry === "." || $entry === "..") { - continue; - } - - $documentPath = $dataPath . $entry; - - try{ - $data = IoHelper::getFileContent($documentPath); - } catch (Exception $exception){ - continue; - } - $data = @json_decode($data, true); - if (!is_array($data)) { - continue; - } - - $storePassed = true; - - // Append only passed data from this store. - - // Process conditions - if(!empty($conditions)) { - // Iterate each conditions. - $storePassed = ConditionsHandler::handleWhereConditions($conditions, $data); - } - - // TODO remove nested where with version 3.0 - $storePassed = ConditionsHandler::handleNestedWhere($data, $storePassed, $nestedWhereConditions); - - if ($storePassed === true && count($distinctFields) > 0) { - $storePassed = ConditionsHandler::handleDistinct($found, $data, $distinctFields); - } - - if ($storePassed === true) { - $found[] = $data; - - // if we just check for existence or want to return the first item, we dont need to look for more documents - if ($getOneDocument === true) { - break; - } - } - } - closedir($handle); - } - - // apply additional changes to result like sort and limit - - if($reduceAndJoinPossible === true){ - DocumentReducer::joinData($found, $listOfJoins); - } - - if (count($found) > 0) { - self::performSearch($found, $search, $searchOptions); - } - - if ($reduceAndJoinPossible === true && !empty($groupBy) && count($found) > 0) { - - DocumentReducer::handleGroupBy( - $found, - $groupBy, - $fieldsToSelect - ); - } - - if($reduceAndJoinPossible === true && empty($groupBy) && count($found) > 0){ - // select specific fields - DocumentReducer::selectFields($found, $primaryKey, $fieldsToSelect); - } - - if(count($found) > 0){ - self::handleHaving($found, $havingConditions); - } - - if($reduceAndJoinPossible === true && count($found) > 0){ - // exclude specific fields - DocumentReducer::excludeFields($found, $fieldsToExclude); - } - - if(count($found) > 0){ - // sort the data. - self::sort($found, $orderBy); - } - - - if(count($found) > 0) { - // Skip data - self::skip($found, $skip); - } - - if(count($found) > 0) { - // Limit data. - self::limit($found, $limit); - } - - return $found; - } - - /** - * @return string - */ - private function getDataPath(): string - { - return $this->storePath . Store::dataDirectory; - } - - /** - * @param array $found - * @param array $orderBy - * @throws InvalidArgumentException - */ - private static function sort(array &$found, array $orderBy){ - if (!empty($orderBy)) { - - $resultSortArray = []; - - foreach ($orderBy as $orderByClause){ - // Start sorting on all data. - $order = $orderByClause['order']; - $fieldName = $orderByClause['fieldName']; - - $arrayColumn = []; - // Get value of the target field. - foreach ($found as $value) { - $arrayColumn[] = NestedHelper::getNestedValue($fieldName, $value); - } - - $resultSortArray[] = $arrayColumn; - - // Decide the order direction. - // order will be asc or desc (check is done in QueryBuilder class) - $resultSortArray[] = ($order === 'asc') ? SORT_ASC : SORT_DESC; - - } - - if(!empty($resultSortArray)){ - $resultSortArray[] = &$found; - array_multisort(...$resultSortArray); - } - unset($resultSortArray); - } - } - - /** - * @param array $found - * @param $skip - */ - private static function skip(array &$found, $skip){ - if (empty($skip) || $skip <= 0) { - return; - } - $found = array_slice($found, $skip); - } - - /** - * @param array $found - * @param $limit - */ - private static function limit(array &$found, $limit){ - if (empty($limit) || $limit <= 0) { - return; - } - $found = array_slice($found, 0, $limit); - } - - /** - * Do a search in store objects. This is like a doing a full-text search. - * @param array $found - * @param array $search - * @param array $searchOptions - * @throws InvalidArgumentException - */ - private static function performSearch(array &$found, array $search, array $searchOptions) - { - if(empty($search)){ - return; - } - $minLength = $searchOptions["minLength"]; - $searchScoreKey = $searchOptions["scoreKey"]; - $searchMode = $searchOptions["mode"]; - $searchAlgorithm = $searchOptions["algorithm"]; - - $scoreMultiplier = 64; - $encoding = "UTF-8"; - - $fields = $search["fields"]; - $query = $search["query"]; - $lowerQuery = mb_strtolower($query, $encoding); - $exactQuery = preg_quote($query, "/"); - - $fieldsLength = count($fields); - - $highestScore = $scoreMultiplier ** $fieldsLength; - - // split query - $searchWords = preg_replace('/(\s)/u', ',', $query); - $searchWords = explode(",", $searchWords); - - $prioritizeAlgorithm = (in_array($searchAlgorithm, [ - Query::SEARCH_ALGORITHM["prioritize"], - Query::SEARCH_ALGORITHM["prioritize_position"] - ], true)); - - $positionAlgorithm = ($searchAlgorithm === Query::SEARCH_ALGORITHM["prioritize_position"]); - - // apply min word length - $temp = []; - foreach ($searchWords as $searchWord){ - if(strlen($searchWord) >= $minLength){ - $temp[] = $searchWord; - } - } - $searchWords = $temp; - unset($temp); - $searchWords = array_map(static function($value){ - return preg_quote($value, "/"); - }, $searchWords); - - // apply mode - if($searchMode === "and"){ - $preg = ""; - foreach ($searchWords as $searchWord){ - $preg .= "(?=.*".$searchWord.")"; - } - $preg = '/^' . $preg . '.*/im'; - $pregOr = '!(' . implode('|', $searchWords) . ')!i'; - } else { - $preg = '!(' . implode('|', $searchWords) . ')!i'; - } - - // search - foreach ($found as $foundKey => &$document) { - $searchHits = 0; - $searchScore = 0; - foreach ($fields as $key => $field) { - if($prioritizeAlgorithm){ - $score = $highestScore / ($scoreMultiplier ** $key); - } else { - $score = $scoreMultiplier; - } - $value = NestedHelper::getNestedValue($field, $document); - - if (!is_string($value) || $value === "") { - continue; - } - - $lowerValue = mb_strtolower($value, $encoding); - - if ($lowerQuery === $lowerValue) { - // exact match - $searchHits++; - $searchScore += 16 * $score; - } elseif ($positionAlgorithm && mb_strpos($lowerValue, $lowerQuery, 0, $encoding) === 0) { - // exact beginning match - $searchHits++; - $searchScore += 8 * $score; - } elseif ($matches = preg_match_all('!' . $exactQuery . '!i', $value)) { - // exact query match - $searchHits += $matches; -// $searchScore += 2 * $score; - $searchScore += $matches * 2 * $score; - if($searchAlgorithm === Query::SEARCH_ALGORITHM["hits_prioritize"]){ - $searchScore += $matches * ($fieldsLength - $key); - } - } - - $matchesArray = []; - - $matches = ($searchMode === "and") ? preg_match($preg, $value) : preg_match_all($preg, $value, $matchesArray, PREG_OFFSET_CAPTURE); - - if ($matches) { - // any match - $searchHits += $matches; - $searchScore += $matches * $score; - if($searchAlgorithm === Query::SEARCH_ALGORITHM["hits_prioritize"]) { - $searchScore += $matches * ($fieldsLength - $key); - } - // because the "and" search algorithm at most finds one match we also use the amount of word occurrences - if($searchMode === "and" && isset($pregOr) && ($matches = preg_match_all($pregOr, $value, $matchesArray, PREG_OFFSET_CAPTURE))){ - $searchHits += $matches; - $searchScore += $matches * $score; - } - } - - // we apply a small very small number to the score to differentiate the distance from the beginning - if($positionAlgorithm && $matches && !empty($matchesArray)){ - $hitPosition = $matchesArray[0][0][1]; - if(!is_int($hitPosition) || !($hitPosition > 0)){ - $hitPosition = 1; - } - $searchScore += ($score / $highestScore) * ($hitPosition / ($hitPosition * $hitPosition)); - } - } - - if($searchHits > 0){ - if(!is_null($searchScoreKey)){ - $document[$searchScoreKey] = $searchScore; - } - } else { - unset($found[$foundKey]); - } - } - } - - /** - * @param array $found - * @param array $havingConditions - * @throws InvalidArgumentException - */ - private static function handleHaving(array &$found, array $havingConditions){ - if(empty($havingConditions)){ - return; - } - - foreach ($found as $key => $document){ - if(false === ConditionsHandler::handleWhereConditions($havingConditions, $document)){ - unset($found[$key]); - } - } - } - -} \ No newline at end of file diff --git a/core/class/sleekDB/Classes/DocumentReducer.php b/core/class/sleekDB/Classes/DocumentReducer.php deleted file mode 100644 index d8615663..00000000 --- a/core/class/sleekDB/Classes/DocumentReducer.php +++ /dev/null @@ -1,665 +0,0 @@ - "avg", - "MAX" => "max", - "MIN" => "min", - "SUM" => "sum", - "ROUND" => "round", - "ABS" => "abs", - "POSITION" => "position", - "UPPER" => "upper", - "LOWER" => "lower", - "LENGTH" => "length", - "CONCAT" => "concat", - "CUSTOM" => "custom", - ]; - - const SELECT_FUNCTIONS_THAT_REDUCE_RESULT = [ - "AVG" => "avg", - "MAX" => "max", - "MIN" => "min", - "SUM" => "sum" - ]; - - /** - * @param array $found - * @param array $fieldsToExclude - */ - public static function excludeFields(array &$found, array $fieldsToExclude){ - if (empty($fieldsToExclude)) { - return; - } - foreach ($found as $key => &$document) { - if(!is_array($document)){ - continue; - } - foreach ($fieldsToExclude as $fieldToExclude) { - NestedHelper::removeNestedField($document, $fieldToExclude); - } - } - } - - /** - * @param array $results - * @param array $listOfJoins - * @throws IOException - * @throws InvalidArgumentException - */ - public static function joinData(array &$results, array $listOfJoins){ - if(empty($listOfJoins)){ - return; - } - // Join data. - foreach ($results as $key => $doc) { - foreach ($listOfJoins as $join) { - // Execute the child query. - $joinQuery = ($join['joinFunction'])($doc); // QueryBuilder or result of fetch - $propertyName = $join['propertyName']; - - // TODO remove SleekDB check in version 3.0 - if($joinQuery instanceof QueryBuilder || $joinQuery instanceof SleekDB){ - $joinResult = $joinQuery->getQuery()->fetch(); - } else if(is_array($joinQuery)){ - // user already fetched the query in the join query function - $joinResult = $joinQuery; - } else { - throw new InvalidArgumentException("Invalid join query."); - } - - // Add child documents with the current document. - $results[$key][$propertyName] = $joinResult; - } - } - } - - /** - * @param array $found - * @param array $groupBy - * @param array $fieldsToSelect - * @throws InvalidArgumentException - */ - public static function handleGroupBy(array &$found, array $groupBy, array $fieldsToSelect) - { - if(empty($groupBy)){ - return; - } - // TODO optimize algorithm if possible - $groupByFields = $groupBy["groupByFields"]; - $countKeyName = $groupBy["countKeyName"]; - $allowEmpty = $groupBy["allowEmpty"]; - - $hasSelectFunctionThatNotReduceResult = false; - $hasSelectFunctionThatReduceResult = false; - - $pattern = (!empty($fieldsToSelect))? $fieldsToSelect : $groupByFields; - - if(!empty($countKeyName) && empty($fieldsToSelect)){ - $pattern[] = $countKeyName; - } - - // remove duplicates - $patternWithOutDuplicates = []; - foreach ($pattern as $key => $value){ - if(array_key_exists($key, $patternWithOutDuplicates) && in_array($value, $patternWithOutDuplicates, true)){ - continue; - } - $patternWithOutDuplicates[$key] = $value; - - // validate pattern - if(!is_string($key) && !is_string($value)){ - throw new InvalidArgumentException("You need to format the select correctly when using Group By."); - } - if(!is_string($value)) { - - if($value instanceof Closure){ - if($hasSelectFunctionThatNotReduceResult === false){ - $hasSelectFunctionThatNotReduceResult = true; - } - if(!in_array($key, $groupByFields, true)){ // key is fieldAlias - throw new InvalidArgumentException("You can not select a field \"$key\" that is not grouped by."); - } - continue; - } - - if (!is_array($value) || empty($value)) { - throw new InvalidArgumentException("You need to format the select correctly when using Group By."); - } - - list($function) = array_keys($value); - $functionParameters = $value[$function]; - self::getFieldNamesOfSelectFunction($function, $functionParameters); - - if(is_string($function) ){ - if(!in_array(strtolower($function), self::SELECT_FUNCTIONS_THAT_REDUCE_RESULT)){ - if($hasSelectFunctionThatNotReduceResult === false){ - $hasSelectFunctionThatNotReduceResult = true; - } - if(!in_array($key, $groupByFields, true)){ // key is fieldAlias - throw new InvalidArgumentException("You can not select a field \"$key\" that is not grouped by."); - } - } else if($hasSelectFunctionThatReduceResult === false){ - $hasSelectFunctionThatReduceResult = true; - } - } - } else if($value !== $countKeyName && !in_array($value, $groupByFields, true)) { - throw new InvalidArgumentException("You can not select a field that is not grouped by."); - } - } - $pattern = $patternWithOutDuplicates; - unset($patternWithOutDuplicates); - - // Apply select functions that do not reduce result before grouping - if($hasSelectFunctionThatNotReduceResult){ - foreach ($found as &$document){ - foreach ($pattern as $key => $value){ - if(is_array($value)){ - list($function) = array_keys($value); - $functionParameters = $value[$function]; - if(in_array(strtolower($function), self::SELECT_FUNCTIONS_THAT_REDUCE_RESULT)){ - continue; - } - - $document[$key] = self::handleSelectFunction($function, $document, $functionParameters); - } else if($value instanceof Closure){ - $function = self::SELECT_FUNCTIONS['CUSTOM']; - $functionParameters = $value; - $document[$key] = self::handleSelectFunction($function, $document, $functionParameters); - } - } - } - unset($document); - } - - // GROUP - $groupedResult = []; - foreach ($found as $foundKey => $document){ - - // Prepare hash for group by - $values = []; - $isEmptyAndEmptyNotAllowed = false; - foreach ($groupByFields as $groupByField){ - $value = NestedHelper::getNestedValue($groupByField, $document); - if($allowEmpty === false && is_null($value)){ - $isEmptyAndEmptyNotAllowed = true; - break; - } - $values[$groupByField] = $value; - } - if($isEmptyAndEmptyNotAllowed === true){ - continue; - } - $valueHash = md5(json_encode($values)); - - // is new entry - if(!array_key_exists($valueHash, $groupedResult)){ - $resultDocument = []; - foreach ($pattern as $key => $patternValue){ - $resultFieldName = (is_string($key)) ? $key : $patternValue; - - if($resultFieldName === $countKeyName){ - // is a counter - $attributeValue = 1; - } else if(!is_string($patternValue)){ - // is a function - list($function) = array_keys($patternValue); - if(in_array(strtolower($function), self::SELECT_FUNCTIONS_THAT_REDUCE_RESULT)){ - // is a select function that reduce result. - $fieldNameToHandle = $patternValue[$function]; - $currentFieldValue = NestedHelper::getNestedValue($fieldNameToHandle, $document); - if(!is_numeric($currentFieldValue)){ - $attributeValue = [$function => [null]]; - } else { - $attributeValue = [$function => [$currentFieldValue]]; - } - } else { - // is a select function that does not reduce result. - $attributeValue = $document[$resultFieldName]; - } - } else { - // is a normal select - $attributeValue = NestedHelper::getNestedValue($patternValue, $document); - } - $resultDocument[$resultFieldName] = $attributeValue; - } - $groupedResult[$valueHash] = $resultDocument; - continue; - } - - // entry exists - $currentResult = $groupedResult[$valueHash]; - foreach ($pattern as $key => $patternValue){ - $resultFieldName = (is_string($key)) ? $key : $patternValue; - - if($resultFieldName === $countKeyName){ - $currentResult[$resultFieldName] += 1; - continue; - } - - if(is_array($patternValue)){ - list($function) = array_keys($patternValue); - if(in_array(strtolower($function), self::SELECT_FUNCTIONS_THAT_REDUCE_RESULT)){ - $fieldNameToHandle = $patternValue[$function]; - $currentFieldValue = NestedHelper::getNestedValue($fieldNameToHandle, $document); - $currentFieldValue = is_numeric($currentFieldValue) ? $currentFieldValue : null; - $currentResult[$resultFieldName][$function][] = $currentFieldValue; - } - } - } - $groupedResult[$valueHash] = $currentResult; - } - - // Apply select functions that reduce result - if($hasSelectFunctionThatReduceResult){ - foreach ($groupedResult as &$document){ - foreach ($pattern as $key => $value){ - if(!is_array($value)){ - continue; - } - list($function) = array_keys($value); - if(!in_array(strtolower($function), self::SELECT_FUNCTIONS_THAT_REDUCE_RESULT)){ - continue; - } - // "price" => ["sum" => [...]] - $functionParameters = $key.".".$function; - $document[$key] = self::handleSelectFunction($function, $document, $functionParameters); - } - } - unset($document); - } - - $found = array_values($groupedResult); - } - - /** - * @param array $found - * @param string $primaryKey - * @param array $fieldsToSelect - * @throws InvalidArgumentException - */ - public static function selectFields(array &$found, string $primaryKey, array $fieldsToSelect) - { - if (empty($fieldsToSelect)) { - return; - } - - $functionsThatReduceResultToSingleResult = self::SELECT_FUNCTIONS_THAT_REDUCE_RESULT; - $reducedResult = []; // "fieldName" => ["values",...] - $reduceResultToSingleResult = false; - - // check if result should be reduced to single result - foreach ($fieldsToSelect as $fieldToSelect){ - if(!is_array($fieldToSelect)){ - continue; - } - - list($function) = array_keys($fieldToSelect); - - if(in_array(strtolower($function), $functionsThatReduceResultToSingleResult, true)){ - $reduceResultToSingleResult = true; - } - - if($reduceResultToSingleResult === true){ - break; - } - } - - // Is not result of group by and contains function that reduces result to single result - if($reduceResultToSingleResult === true){ - foreach ($found as $key => $document) { - foreach ($fieldsToSelect as $fieldAlias => $fieldToSelect) { - $fieldName = (!is_int($fieldAlias))? $fieldAlias : $fieldToSelect; - if(!is_array($fieldToSelect)){ - continue; - } - - // no alias specified and select function (array) used as element - if(!is_string($fieldName)){ - $errorMsg = "You need to specify an alias for the field when using select functions."; - throw new InvalidArgumentException($errorMsg); - } - - list($function) = array_keys($fieldToSelect); - $functionParameters = $fieldToSelect[$function]; - - if(in_array(strtolower($function), $functionsThatReduceResultToSingleResult, true)){ - if(!is_string($functionParameters)){ - $errorMsg = "When using the function \"$function\" the parameter has to be a string (fieldName)."; - throw new InvalidArgumentException($errorMsg); - } - - $value = NestedHelper::getNestedValue($functionParameters, $document); - if(!array_key_exists($fieldName, $reducedResult)){ - $reducedResult[$fieldName] = []; - } - $reducedResult[$fieldName][] = $value; - } - } - } - - $newDocument = []; - foreach ($fieldsToSelect as $fieldAlias => $fieldToSelect){ - $fieldName = (!is_int($fieldAlias))? $fieldAlias : $fieldToSelect; - if(!is_array($fieldToSelect)){ - continue; - } - - list($function) = array_keys($fieldToSelect); - if(in_array(strtolower($function), $functionsThatReduceResultToSingleResult, true)){ - $newDocument[$fieldName] = self::handleSelectFunction($function, $reducedResult, $fieldName); - } - } - $found = [$newDocument]; - return; - } - - // result should not be reduced to single result - - foreach ($found as $key => &$document) { - $newDocument = []; - - $newDocument[$primaryKey] = $document[$primaryKey]; - foreach ($fieldsToSelect as $fieldAlias => $fieldToSelect) { - - $fieldName = (!is_int($fieldAlias))? $fieldAlias : $fieldToSelect; - - if(!is_string($fieldToSelect) && !is_int($fieldToSelect) && !is_array($fieldToSelect) - && !($fieldToSelect instanceof Closure)) - { - $errorMsg = "When using select an array containing fieldNames as strings or select functions has to be given"; - throw new InvalidArgumentException($errorMsg); - } - - // no alias specified and select function (array) used as element - if(!is_string($fieldName)){ - $errorMsg = "You need to specify an alias for the field when using select functions."; - throw new InvalidArgumentException($errorMsg); - } - - // if the fieldToSelect is an array, the user wants to use a select function - if(is_array($fieldToSelect)){ - // "fieldAlias" => ["function" => "field"] - list($function) = array_keys($fieldToSelect); - $functionParameters = $fieldToSelect[$function]; - $newDocument[$fieldName] = self::handleSelectFunction($function, $document, $functionParameters); - } else if($fieldToSelect instanceof Closure){ - $function = self::SELECT_FUNCTIONS['CUSTOM']; - $functionParameters = $fieldToSelect; - $newDocument[$fieldName] = self::handleSelectFunction($function, $document, $functionParameters); - } else { - // No select function is used (fieldToSelect is string or int) - $fieldValue = NestedHelper::getNestedValue((string) $fieldToSelect, $document); - $createdArray = NestedHelper::createNestedArray($fieldName, $fieldValue); - if(!empty($createdArray)){ - $createdArrayKey = array_keys($createdArray)[0]; - $newDocument[$createdArrayKey] = $createdArray[$createdArrayKey]; - } - } - } - $document = $newDocument; - } - } - - /** - * @param string $function - * @param array $document - * @param string|array|int|Closure $functionParameters - * @return mixed - * @throws InvalidArgumentException - */ - private static function handleSelectFunction(string $function, array $document, $functionParameters){ - - if(is_int($functionParameters)){ - $functionParameters = (string) $functionParameters; - } - - switch (strtolower($function)){ - case self::SELECT_FUNCTIONS["ROUND"]: - list($field, $precision) = self::getFieldNamesOfSelectFunction($function, $functionParameters); - if(!is_string($field) || !is_int($precision)){ - $errorMsg = "When using the select function \"$function\" the field parameter has to be a string " - ."and the precision parameter has to be an integer"; - throw new InvalidArgumentException($errorMsg); - } - - $data = NestedHelper::getNestedValue($field, $document); - if(!is_numeric($data)){ - return null; - } - return round((float) $data, $precision); - case self::SELECT_FUNCTIONS["ABS"]: - list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters); - $data = NestedHelper::getNestedValue($field, $document); - if(!is_numeric($data)){ - return null; - } - return abs($data); - case self::SELECT_FUNCTIONS["POSITION"]: - list($field, $subString) = self::getFieldNamesOfSelectFunction($function, $functionParameters); - if(!is_string($subString) || !is_string($field)){ - $errorMsg = "When using the select function \"$function\" the subString and field parameters has to be strings"; - throw new InvalidArgumentException($errorMsg); - } - - $data = NestedHelper::getNestedValue($field, $document); - if(!is_string($data)){ - return null; - } - $result = strpos($data, $subString); - return ($result !== false)? $result + 1 : null; - case self::SELECT_FUNCTIONS["UPPER"]: - list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters); - $data = NestedHelper::getNestedValue($field, $document); - if(!is_string($data)){ - return null; - } - return strtoupper($data); - case self::SELECT_FUNCTIONS["LOWER"]: - list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters); - $data = NestedHelper::getNestedValue($field, $document); - if(!is_string($data)){ - return null; - } - return strtolower($data); - case self::SELECT_FUNCTIONS["LENGTH"]: - list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters); - $data = NestedHelper::getNestedValue($field, $document); - if(is_string($data)){ - return strlen($data); - } - if(is_array($data)){ - return count($data); - } - return null; - case self::SELECT_FUNCTIONS["CONCAT"]: - list($fields, $glue) = self::getFieldNamesOfSelectFunction($function, $functionParameters); - $result = ""; - foreach ($fields as $field){ - $data = NestedHelper::getNestedValue($field, $document); - // convertible to string - if( - ( !is_array( $data ) ) - && ($data !== "" && $data !== null) - && ( - ( !is_object( $data ) && settype( $data, 'string' ) !== false ) - || ( is_object( $data ) && method_exists( $data, '__toString' ) ) - ) - ) - { - if($result !== ""){ - $result .= $glue; - } - $result .= $data; - } - } - return ($result !== "") ? $result : null; - case self::SELECT_FUNCTIONS["SUM"]: - list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters); - $data = NestedHelper::getNestedValue($field, $document); - if(!is_array($data)){ - return null; - } - - $result = 0; - $allEntriesNull = true; - foreach ($data as $value){ - if(!is_null($value)){ - $result += $value; - $allEntriesNull = false; - } - } - if($allEntriesNull === true){ - return null; - } - return $result; - case self::SELECT_FUNCTIONS["MIN"]: - list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters); - $data = NestedHelper::getNestedValue($field, $document); - if(!is_array($data)){ - return null; - } - - $result = INF; - $allEntriesNull = true; - foreach ($data as $value){ - if(!is_null($value)){ - if($value < $result){ - $result = $value; - } - $allEntriesNull = false; - } - } - if($allEntriesNull === true){ - return null; - } - return $result; - case self::SELECT_FUNCTIONS["MAX"]: - list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters); - $data = NestedHelper::getNestedValue($field, $document); - if(!is_array($data)){ - return null; - } - - $result = -INF; - $allEntriesNull = true; - foreach ($data as $value){ - if($value > $result && !is_null($value)) { - $result = $value; - $allEntriesNull = false; - } - } - if($allEntriesNull === true){ - return null; - } - return $result; - case self::SELECT_FUNCTIONS["AVG"]: - list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters); - $data = NestedHelper::getNestedValue($field, $document); - if(!is_array($data)){ - return null; - } - - $result = 0; - $resultValueAmount = (count($data) + 1); - $allEntriesNull = true; - foreach ($data as $value){ - if(!is_null($value)){ - $result += $value; - $allEntriesNull = false; - } - } - if($allEntriesNull === true){ - return null; - } - return ($result / $resultValueAmount); - case self::SELECT_FUNCTIONS['CUSTOM']: - if(!($functionParameters instanceof Closure)){ - throw new InvalidArgumentException("When using a custom select function you need to provide a closure."); - } - return $functionParameters($document); - default: - throw new InvalidArgumentException("The given select function \"$function\" is not supported."); - } - } - - /** - * @param string $function - * @param string|array|int $functionParameters - * @return array [array|string $fieldNames, $addition] - * @throws InvalidArgumentException - */ - private static function getFieldNamesOfSelectFunction(string $function, $functionParameters): array - { - if(is_int($functionParameters)){ - $functionParameters = (string) $functionParameters; - } - $function = strtolower($function); - switch ($function){ - case self::SELECT_FUNCTIONS["ROUND"]: - case self::SELECT_FUNCTIONS["POSITION"]: - if(!is_array($functionParameters) || count($functionParameters) !== 2){ - $type = gettype($functionParameters); - $length = (is_array($functionParameters)) ? count($functionParameters) : 0; - $errorMsg = "When using the select function \"$function\" the parameter " - ."has to be an array with length = 2, got $type with length $length"; - throw new InvalidArgumentException($errorMsg); - } - list($firstParameter, $secondParameter) = $functionParameters; - if($function === self::SELECT_FUNCTIONS["ROUND"]){ - $field = $firstParameter; - $addition = $secondParameter; - } else { - $field = $secondParameter; - $addition = $firstParameter; - } - return [$field, $addition]; - case self::SELECT_FUNCTIONS["ABS"]: - case self::SELECT_FUNCTIONS["UPPER"]: - case self::SELECT_FUNCTIONS["LOWER"]: - case self::SELECT_FUNCTIONS["LENGTH"]: - case self::SELECT_FUNCTIONS["SUM"]: - case self::SELECT_FUNCTIONS["MIN"]: - case self::SELECT_FUNCTIONS["MAX"]: - case self::SELECT_FUNCTIONS["AVG"]: - if(!is_string($functionParameters)){ - $type = gettype($functionParameters); - $errorMsg = "When using the select function \"$function\" the parameter " - ."has to be a string, got $type."; - throw new InvalidArgumentException($errorMsg); - } - return [$functionParameters, null]; - case self::SELECT_FUNCTIONS["CONCAT"]: - if(!is_array($functionParameters) || count($functionParameters) < 3){ - $type = gettype($functionParameters); - $length = (is_array($functionParameters)) ? count($functionParameters) : 0; - $errorMsg = "When using the select function \"$function\" the parameter " - ."has to be an array with length > 3, got $type with length $length"; - throw new InvalidArgumentException($errorMsg); - } - list($glue) = $functionParameters; - unset($functionParameters[array_keys($functionParameters)[0]]); - - return [$functionParameters, $glue]; - default: - throw new InvalidArgumentException("The given select function \"$function\" is not supported."); - } - } - -} \ No newline at end of file diff --git a/core/class/sleekDB/Classes/DocumentUpdater.php b/core/class/sleekDB/Classes/DocumentUpdater.php deleted file mode 100644 index 20fd98c2..00000000 --- a/core/class/sleekDB/Classes/DocumentUpdater.php +++ /dev/null @@ -1,159 +0,0 @@ -storePath = $storePath; - $this->primaryKey = $primaryKey; - } - - /** - * Update one or multiple documents, based on current query - * @param array $results - * @param array $updatable - * @param bool $returnUpdatedDocuments - * @return array|bool - * @throws IOException - */ - public function updateResults(array $results, array $updatable, bool $returnUpdatedDocuments) - { - if(count($results) === 0) { - return false; - } - - $primaryKey = $this->primaryKey; - $dataPath = $this->getDataPath(); - // check if all documents exist beforehand - foreach ($results as $key => $data) { - $primaryKeyValue = IoHelper::secureStringForFileAccess($data[$primaryKey]); - $data[$primaryKey] = (int) $primaryKeyValue; - $results[$key] = $data; - - $filePath = $dataPath . $primaryKeyValue . '.json'; - if(!file_exists($filePath)){ - return false; - } - } - - foreach ($results as $key => $data){ - $filePath = $dataPath . $data[$primaryKey] . '.json'; - foreach ($updatable as $fieldName => $value) { - // Do not update the primary key reserved index of a store. - if ($fieldName !== $primaryKey) { - NestedHelper::updateNestedValue($fieldName, $data, $value); - } - } - IoHelper::writeContentToFile($filePath, json_encode($data)); - $results[$key] = $data; - } - return ($returnUpdatedDocuments === true) ? $results : true; - } - - /** - * Deletes matched store objects. - * @param array $results - * @param int $returnOption - * @return bool|array|int - * @throws IOException - * @throws InvalidArgumentException - */ - public function deleteResults(array $results, int $returnOption) - { - $primaryKey = $this->primaryKey; - $dataPath = $this->getDataPath(); - switch ($returnOption){ - case Query::DELETE_RETURN_BOOL: - $returnValue = !empty($results); - break; - case Query::DELETE_RETURN_COUNT: - $returnValue = count($results); - break; - case Query::DELETE_RETURN_RESULTS: - $returnValue = $results; - break; - default: - throw new InvalidArgumentException("Return option \"$returnOption\" is not supported"); - } - - if (empty($results)) { - return $returnValue; - } - - // TODO implement beforehand check - - foreach ($results as $key => $data) { - $primaryKeyValue = IoHelper::secureStringForFileAccess($data[$primaryKey]); - $filePath = $dataPath . $primaryKeyValue . '.json'; - if(false === IoHelper::deleteFile($filePath)){ - throw new IOException( - 'Unable to delete document! - Already deleted documents: '.$key.'. - Location: "' . $filePath .'"' - ); - } - } - return $returnValue; - } - - /** - * @param array $results - * @param array $fieldsToRemove - * @return array|false - * @throws IOException - */ - public function removeFields(array &$results, array $fieldsToRemove) - { - $primaryKey = $this->primaryKey; - $dataPath = $this->getDataPath(); - - // check if all documents exist beforehand - foreach ($results as $key => $data) { - $primaryKeyValue = IoHelper::secureStringForFileAccess($data[$primaryKey]); - $data[$primaryKey] = $primaryKeyValue; - $results[$key] = $data; - - $filePath = $dataPath . $primaryKeyValue . '.json'; - if(!file_exists($filePath)){ - return false; - } - } - - foreach ($results as &$document){ - foreach ($fieldsToRemove as $fieldToRemove){ - if($fieldToRemove !== $primaryKey){ - NestedHelper::removeNestedField($document, $fieldToRemove); - } - } - $filePath = $dataPath . $document[$primaryKey] . '.json'; - IoHelper::writeContentToFile($filePath, json_encode($document)); - } - return $results; - } - - /** - * @return string - */ - private function getDataPath(): string - { - return $this->storePath . Store::dataDirectory; - } - -} \ No newline at end of file diff --git a/core/class/sleekDB/Classes/IoHelper.php b/core/class/sleekDB/Classes/IoHelper.php deleted file mode 100644 index 376b53b5..00000000 --- a/core/class/sleekDB/Classes/IoHelper.php +++ /dev/null @@ -1,251 +0,0 @@ -isDir()) { - rmdir($file->getRealPath()); - } else { - unlink($file->getRealPath()); - } - } - return rmdir($folderPath); - } - - /** - * @param string $folderPath - * @throws IOException - */ - public static function createFolder(string $folderPath){ - // We don't need to create a folder if it already exists. - if(file_exists($folderPath) === true){ - return; - } - self::checkWrite($folderPath); - // Check if the data_directory exists or create one. - if (!file_exists($folderPath) && !mkdir($folderPath, 0777, true) && !is_dir($folderPath)) { - throw new IOException( - 'Unable to create the a directory at ' . $folderPath - ); - } - } - - /** - * @param string $filePath - * @param Closure $updateContentFunction Has to return a string or an array that will be encoded to json. - * @return string - * @throws IOException - * @throws JsonException - */ - public static function updateFileContent(string $filePath, Closure $updateContentFunction): string - { - self::checkRead($filePath); - self::checkWrite($filePath); - - $content = false; - - $fp = fopen($filePath, 'rb'); - if(flock($fp, LOCK_SH)){ - $content = stream_get_contents($fp); - } - flock($fp, LOCK_UN); - fclose($fp); - - if($content === false){ - throw new IOException("Could not get shared lock for file: $filePath"); - } - - $content = $updateContentFunction($content); - - if(!is_string($content)){ - $encodedContent = json_encode($content); - if($encodedContent === false){ - $content = (!is_object($content) && !is_array($content) && !is_null($content)) ? $content : gettype($content); - throw new JsonException("Could not encode content with json_encode. Content: \"$content\"."); - } - $content = $encodedContent; - } - - - if(file_put_contents($filePath, $content, LOCK_EX) === false){ - throw new IOException("Could not write content to file. Please check permissions at: $filePath"); - } - - - return $content; - } - - /** - * @param string $filePath - * @return bool - */ - public static function deleteFile(string $filePath): bool - { - - if(false === file_exists($filePath)){ - return true; - } - try{ - self::checkWrite($filePath); - }catch(Exception $exception){ - return false; - } - - return (@unlink($filePath) && !file_exists($filePath)); - } - - /** - * @param array $filePaths - * @return bool - */ - public static function deleteFiles(array $filePaths): bool - { - foreach ($filePaths as $filePath){ - // if a file does not exist, we do not need to delete it. - if(true === file_exists($filePath)){ - try{ - self::checkWrite($filePath); - if(false === @unlink($filePath) || file_exists($filePath)){ - return false; - } - } catch (Exception $exception){ - // TODO trigger a warning or exception - return false; - } - } - } - return true; - } - - /** - * Strip string for secure file access. - * @param string $string - * @return string - */ - public static function secureStringForFileAccess(string $string): string - { - return (str_replace(array(".", "/", "\\"), "", $string)); - } - - /** - * Appends a slash ("/") to the given directory path if there is none. - * @param string $directory - */ - public static function normalizeDirectory(string &$directory){ - if(!empty($directory) && substr($directory, -1) !== "/") { - $directory .= "/"; - } - } - - /** - * Returns the amount of files in folder. - * @param string $folder - * @return int - * @throws IOException - */ - public static function countFolderContent(string $folder): int - { - self::checkRead($folder); - $fi = new \FilesystemIterator($folder, \FilesystemIterator::SKIP_DOTS); - return iterator_count($fi); - } -} \ No newline at end of file diff --git a/core/class/sleekDB/Classes/NestedHelper.php b/core/class/sleekDB/Classes/NestedHelper.php deleted file mode 100644 index 6c1979fc..00000000 --- a/core/class/sleekDB/Classes/NestedHelper.php +++ /dev/null @@ -1,140 +0,0 @@ - 1){ - $data = self::_updateNestedValueHelper($fieldNameArray, $data, $newValue, count($fieldNameArray)); - return; - } - $data[$fieldNameArray[0]] = $value; - } - - public static function createNestedArray(string $fieldName, $fieldValue): array - { - $temp = []; - $fieldNameArray = explode('.', $fieldName); - $fieldNameArrayReverse = array_reverse($fieldNameArray); - foreach ($fieldNameArrayReverse as $index => $i) { - if($index === 0){ - $temp = array($i => $fieldValue); - } else { - $temp = array($i => $temp); - } - } - - return $temp; - } - - public static function removeNestedField(array &$document, string $fieldToRemove){ - if (array_key_exists($fieldToRemove, $document)) { - unset($document[$fieldToRemove]); - return; - } - // should be a nested array at this point - $temp = &$document; - $fieldNameArray = explode('.', $fieldToRemove); - $fieldNameArrayCount = count($fieldNameArray); - foreach ($fieldNameArray as $index => $i) { - // last iteration - if(($fieldNameArrayCount - 1) === $index){ - if(is_array($temp) && array_key_exists($i, $temp)) { - unset($temp[$i]); - } - break; - } - if(!is_array($temp) || !array_key_exists($i, $temp)){ - break; - } - $temp = &$temp[$i]; - } - } - - /** - * @param array $keysArray - * @param $data - * @param $newValue - * @param int $originalKeySize - * @return mixed - */ - private static function _updateNestedValueHelper(array $keysArray, $data, $newValue, int $originalKeySize) - { - if(empty($keysArray)){ - return $newValue; - } - $currentKey = $keysArray[0]; - $result = (is_array($data)) ? $data : []; - if(!is_array($data) || !array_key_exists($currentKey, $data)){ - $result[$currentKey] = self::_updateNestedValueHelper(array_slice($keysArray, 1), $data, $newValue, $originalKeySize); - if(count($keysArray) !== $originalKeySize){ - return $result; - } - } - $result[$currentKey] = self::_updateNestedValueHelper(array_slice($keysArray, 1), $data[$currentKey], $newValue, $originalKeySize); - return $result; - } -} \ No newline at end of file diff --git a/core/class/sleekDB/Exceptions/IOException.php b/core/class/sleekDB/Exceptions/IOException.php deleted file mode 100644 index cb634625..00000000 --- a/core/class/sleekDB/Exceptions/IOException.php +++ /dev/null @@ -1,7 +0,0 @@ - 1, - "hits_prioritize" => 2, - "prioritize" => 3, - "prioritize_position" => 4, - ]; - - /** - * @var CacheHandler|null - */ - protected $cacheHandler; - - /** - * @var DocumentFinder - */ - protected $documentFinder; - - /** - * @var DocumentUpdater - */ - protected $documentUpdater; - - /** - * Query constructor. - * @param QueryBuilder $queryBuilder - */ - public function __construct(QueryBuilder $queryBuilder) - { - $store = $queryBuilder->_getStore(); - $primaryKey = $store->getPrimaryKey(); - - $this->cacheHandler = new CacheHandler($store->getStorePath(), $queryBuilder); - $this->documentFinder = new DocumentFinder($store->getStorePath(), $queryBuilder->_getConditionProperties(), $primaryKey); - $this->documentUpdater = new DocumentUpdater($store->getStorePath(), $primaryKey); - } - - /** - * Execute query and get results. - * @return array - * @throws InvalidArgumentException - * @throws IOException - */ - public function fetch(): array - { - return $this->getResults(); - } - - /** - * Check if data is found. - * @return bool - * @throws InvalidArgumentException - * @throws IOException - */ - public function exists(): bool - { - // Return boolean on data exists check. - return !empty($this->first()); - } - - /** - * Return the first document. - * @return array empty array or single document - * @throws InvalidArgumentException - * @throws IOException - */ - public function first(): array - { - return $this->getResults(true); - } - - /** - * Update parts of one or multiple documents based on current query. - * @param array $updatable - * @param bool $returnUpdatedDocuments - * @return array|bool - * @throws InvalidArgumentException - * @throws IOException - */ - public function update(array $updatable, bool $returnUpdatedDocuments = false){ - - if(empty($updatable)){ - throw new InvalidArgumentException("You have to define what you want to update."); - } - - $results = $this->documentFinder->findDocuments(false, false); - - $this->getCacheHandler()->deleteAllWithNoLifetime(); - - return $this->documentUpdater->updateResults($results, $updatable, $returnUpdatedDocuments); - } - - /** - * Delete one or multiple documents based on current query. - * @param int $returnOption - * @return bool|array|int - * @throws InvalidArgumentException - * @throws IOException - */ - public function delete(int $returnOption = self::DELETE_RETURN_BOOL){ - $results = $this->documentFinder->findDocuments(false, false); - - $this->getCacheHandler()->deleteAllWithNoLifetime(); - - return $this->documentUpdater->deleteResults($results, $returnOption); - } - - /** - * Remove fields of one or multiple documents based on current query. - * @param array $fieldsToRemove - * @return array|false - * @throws IOException - * @throws InvalidArgumentException - */ - public function removeFields(array $fieldsToRemove) - { - if(empty($fieldsToRemove)){ - throw new InvalidArgumentException("You have to define what fields you want to remove."); - } - $results = $this->documentFinder->findDocuments(false, false); - - $this->getCacheHandler()->deleteAllWithNoLifetime(); - - return $this->documentUpdater->removeFields($results, $fieldsToRemove); - } - - /** - * Retrieve Cache object. - * @return Cache - */ - public function getCache(): Cache - { - return $this->getCacheHandler()->getCache(); - } - - /** - * Retrieve the results from either the cache or store. - * @param bool $getOneDocument - * @return array - * @throws IOException - * @throws InvalidArgumentException - */ - private function getResults(bool $getOneDocument = false): array - { - - $results = $this->getCacheHandler()->getCacheContent($getOneDocument); - - if($results !== null) { - return $results; - } - - $results = $this->documentFinder->findDocuments($getOneDocument, true); - - if ($getOneDocument === true && count($results) > 0) { - list($item) = $results; - $results = $item; - } - - $this->getCacheHandler()->setCacheContent($results); - - return $results; - } - - /** - * Retrieve the caching layer bridge. - * @return CacheHandler - */ - private function getCacheHandler(): CacheHandler - { - return $this->cacheHandler; - } -} \ No newline at end of file diff --git a/core/class/sleekDB/QueryBuilder.php b/core/class/sleekDB/QueryBuilder.php deleted file mode 100644 index d9f711fd..00000000 --- a/core/class/sleekDB/QueryBuilder.php +++ /dev/null @@ -1,516 +0,0 @@ - 2, - "scoreKey" => "searchScore", - "mode" => "or", - "algorithm" => Query::SEARCH_ALGORITHM["hits"] - ]; - - protected $fieldsToSelect = []; - protected $fieldsToExclude = []; - protected $groupBy = []; - protected $havingConditions = []; - - protected $listOfJoins = []; - protected $distinctFields = []; - - protected $useCache; - protected $regenerateCache = false; - protected $cacheLifetime; - - - // will also not be used for cache token - protected $propertiesNotUsedInConditionsArray = [ - "propertiesNotUsedInConditionsArray", - "propertiesNotUsedForCacheToken", - "store", - "cache", - ]; - - protected $propertiesNotUsedForCacheToken = [ - "useCache", - "regenerateCache", - "cacheLifetime" - ]; - - /** - * QueryBuilder constructor. - * @param Store $store - */ - public function __construct(Store $store) - { - $this->store = $store; - $this->useCache = $store->_getUseCache(); - $this->cacheLifetime = $store->_getDefaultCacheLifetime(); - $this->searchOptions = $store->_getSearchOptions(); - } - - /** - * Select specific fields - * @param array $fieldNames - * @return QueryBuilder - */ - public function select(array $fieldNames): QueryBuilder - { - foreach ($fieldNames as $key => $fieldName) { - if(is_string($key)){ - $this->fieldsToSelect[$key] = $fieldName; - } else { - $this->fieldsToSelect[] = $fieldName; - } - } - return $this; - } - - /** - * Exclude specific fields - * @param string[] $fieldNames - * @return QueryBuilder - * @throws InvalidArgumentException - */ - public function except(array $fieldNames): QueryBuilder - { - $errorMsg = "If except is used an array containing strings with fieldNames has to be given"; - foreach ($fieldNames as $fieldName) { - if (empty($fieldName)) { - continue; - } - if (!is_string($fieldName)) { - throw new InvalidArgumentException($errorMsg); - } - $this->fieldsToExclude[] = $fieldName; - } - return $this; - } - - /** - * Add conditions to filter data. - * @param array $conditions - * @return QueryBuilder - * @throws InvalidArgumentException - */ - public function where(array $conditions): QueryBuilder - { - if (empty($conditions)) { - throw new InvalidArgumentException("You need to specify a where clause"); - } - - $this->whereConditions[] = $conditions; - - return $this; - } - - /** - * Add or-where conditions to filter data. - * @param array $conditions array(array(string fieldName, string condition, mixed value) [, array(...)]) - * @return QueryBuilder - * @throws InvalidArgumentException - */ - public function orWhere(array $conditions): QueryBuilder - { - - if (empty($conditions)) { - throw new InvalidArgumentException("You need to specify a where clause"); - } - - $this->whereConditions[] = "or"; - $this->whereConditions[] = $conditions; - - return $this; - } - - /** - * Set the amount of data record to skip. - * @param int|string $skip - * @return QueryBuilder - * @throws InvalidArgumentException - */ - public function skip($skip = 0): QueryBuilder - { - if((!is_string($skip) || !is_numeric($skip)) && !is_int($skip)){ - throw new InvalidArgumentException("Skip has to be an integer or a numeric string"); - } - - if(!is_int($skip)){ - $skip = (int) $skip; - } - - if($skip < 0){ - throw new InvalidArgumentException("Skip has to be an integer >= 0"); - } - - $this->skip = $skip; - - return $this; - } - - /** - * Set the amount of data record to limit. - * @param int|string $limit - * @return QueryBuilder - * @throws InvalidArgumentException - */ - public function limit($limit = 0): QueryBuilder - { - - if((!is_string($limit) || !is_numeric($limit)) && !is_int($limit)){ - throw new InvalidArgumentException("Limit has to be an integer or a numeric string"); - } - - if(!is_int($limit)){ - $limit = (int) $limit; - } - - if($limit <= 0){ - throw new InvalidArgumentException("Limit has to be an integer > 0"); - } - - $this->limit = $limit; - - return $this; - } - - /** - * Set the sort order. - * @param array $criteria to order by. array($fieldName => $order). $order can be "asc" or "desc" - * @return QueryBuilder - * @throws InvalidArgumentException - */ - public function orderBy( array $criteria): QueryBuilder - { - foreach ($criteria as $fieldName => $order){ - - if(!is_string($order)) { - throw new InvalidArgumentException('Order has to be a string! Please use "asc" or "desc" only.'); - } - - $order = strtolower($order); - - if(!is_string($fieldName)) { - throw new InvalidArgumentException("Field name has to be a string"); - } - - if (!in_array($order, ['asc', 'desc'])) { - throw new InvalidArgumentException('Please use "asc" or "desc" only.'); - } - - $this->orderBy[] = [ - 'fieldName' => $fieldName, - 'order' => $order - ]; - } - - return $this; - } - - /** - * Do a fulltext like search against one or multiple fields. - * @param string|array $fields one or multiple fieldNames as an array - * @param string $query - * @param array $options - * @return QueryBuilder - * @throws InvalidArgumentException - */ - public function search($fields, string $query, array $options = []): QueryBuilder - { - if(!is_array($fields) && !is_string($fields)){ - throw new InvalidArgumentException("Fields to search through have to be either a string or an array."); - } - - if(!is_array($fields)){ - $fields = (array)$fields; - } - - if (empty($fields)) { - throw new InvalidArgumentException('Cant perform search due to no field name was provided'); - } - - if(count($fields) > 100){ - trigger_error('Searching through more than 100 fields is not recommended and can be resource heavy.', E_USER_WARNING); - } - - if (!empty($query)) { - $this->search = [ - 'fields' => $fields, - 'query' => $query - ]; - if(!empty($options)){ - if(array_key_exists("minLength", $options) && is_int($options["minLength"]) && $options["minLength"] > 0){ - $this->searchOptions["minLength"] = $options["minLength"]; - } - if(array_key_exists("mode", $options) && is_string($options["mode"])){ - $searchMode = strtolower(trim($options["mode"])); - if(in_array($searchMode, ["and", "or"])){ - $this->searchOptions["mode"] = $searchMode; - } - } - if(array_key_exists("scoreKey", $options) && (is_string($options["scoreKey"]) || is_null($options["scoreKey"]))){ - $this->searchOptions["scoreKey"] = $options["scoreKey"]; - } - if(array_key_exists("algorithm", $options) && in_array($options["algorithm"], Query::SEARCH_ALGORITHM, true)){ - $this->searchOptions["algorithm"] = $options["algorithm"]; - } - } - } - return $this; - } - - /** - * @param Closure $joinFunction - * @param string $propertyName - * @return QueryBuilder - */ - public function join(Closure $joinFunction, string $propertyName): QueryBuilder - { - $this->listOfJoins[] = [ - 'propertyName' => $propertyName, - 'joinFunction' => $joinFunction - ]; - return $this; - } - - - /** - * Return distinct values. - * @param array|string $fields - * @return QueryBuilder - * @throws InvalidArgumentException - */ - public function distinct($fields = []): QueryBuilder - { - $fieldType = gettype($fields); - if ($fieldType === 'array') { - if ($fields === array_values($fields)) { - // Append fields. - $this->distinctFields = array_merge($this->distinctFields, $fields); - } else { - throw new InvalidArgumentException( - 'Field value in distinct() method can not be an associative array, - please provide a string or a list of string as a non-associative array.' - ); - } - } else if ($fieldType === 'string' && !empty($fields)) { - $this->distinctFields[] = trim($fields); - } else { - throw new InvalidArgumentException( - 'Field value in distinct() is invalid.' - ); - } - return $this; - } - - /** - * Use caching for current query - * @param null|int $lifetime time to live as int in seconds or null to regenerate cache on every insert, update and delete - * @return QueryBuilder - * @throws InvalidArgumentException - */ - public function useCache(int $lifetime = null): QueryBuilder - { - $this->useCache = true; - if((!is_int($lifetime) || $lifetime < 0) && !is_null($lifetime)){ - throw new InvalidArgumentException("lifetime has to be int >= 0 or null"); - } - $this->cacheLifetime = $lifetime; - return $this; - } - - /** - * Disable cache for the query. - * @return QueryBuilder - */ - public function disableCache(): QueryBuilder - { - $this->useCache = false; - return $this; - } - - /** - * Re-generate the cache for the query. - * @return QueryBuilder - */ - public function regenerateCache(): QueryBuilder - { - $this->regenerateCache = true; - return $this; - } - - /** - * @return Query - */ - public function getQuery(): Query - { - return new Query($this); - } - - /** - * @param array $groupByFields - * @param string|null $countKeyName - * @param bool $allowEmpty - * @return QueryBuilder - */ - public function groupBy(array $groupByFields, string $countKeyName = null, bool $allowEmpty = false): QueryBuilder - { - $this->groupBy = [ - "groupByFields" => $groupByFields, - "countKeyName" => $countKeyName, - "allowEmpty" => $allowEmpty - ]; - return $this; - } - - /** - * Filter result data of groupBy - * @param array $criteria - * @return QueryBuilder - * @throws InvalidArgumentException - */ - public function having(array $criteria): QueryBuilder - { - if (empty($criteria)) { - throw new InvalidArgumentException("You need to specify a having clause"); - } - $this->havingConditions = $criteria; - return $this; - } - - /** - * Returns a an array used to generate a unique token for the current query. - * @return array - */ - public function _getCacheTokenArray(): array - { - $properties = []; - $conditionsArray = $this->_getConditionProperties(); - - foreach ($conditionsArray as $propertyName => $propertyValue){ - if(!in_array($propertyName, $this->propertiesNotUsedForCacheToken, true)){ - $properties[$propertyName] = $propertyValue; - } - } - - return $properties; - } - - /** - * Returns an array containing all information needed to execute an query. - * @return array - */ - public function _getConditionProperties(): array - { - $allProperties = get_object_vars($this); - $properties = []; - - foreach ($allProperties as $propertyName => $propertyValue){ - if(!in_array($propertyName, $this->propertiesNotUsedInConditionsArray, true)){ - $properties[$propertyName] = $propertyValue; - } - } - - return $properties; - } - - /** - * Returns the Store object used to create the QueryBuilder object. - * @return Store - */ - public function _getStore(): Store{ - return $this->store; - } - - /** - * Add "in" condition to filter data. - * @param string $fieldName - * @param array $values - * @return QueryBuilder - * @throws InvalidArgumentException - * @deprecated since version 2.4, use where and orWhere instead. - */ - public function in(string $fieldName, array $values = []): QueryBuilder - { - if (empty($fieldName)) { - throw new InvalidArgumentException('Field name for in clause can not be empty.'); - } - - // Add to conditions with "AND" operation - $this->whereConditions[] = [$fieldName, "in", $values]; - return $this; - } - - /** - * Add "not in" condition to filter data. - * @param string $fieldName - * @param array $values - * @return QueryBuilder - * @throws InvalidArgumentException - * @deprecated since version 2.4, use where and orWhere instead. - */ - public function notIn(string $fieldName, array $values = []): QueryBuilder - { - if (empty($fieldName)) { - throw new InvalidArgumentException('Field name for notIn clause can not be empty.'); - } - - // Add to conditions with "AND" operation - $this->whereConditions[] = [$fieldName, "not in", $values]; - return $this; - } - - /** - * Add a where statement that is nested. ( $x or ($y and $z) ) - * @param array $conditions - * @return QueryBuilder - * @throws InvalidArgumentException - * @deprecated since version 2.3, use where or orWhere instead. - */ - public function nestedWhere(array $conditions): QueryBuilder - { - // TODO remove with version 3.0 - if(empty($conditions)){ - throw new InvalidArgumentException("You need to specify nested where clauses"); - } - - if(count($conditions) > 1){ - throw new InvalidArgumentException("You are not allowed to specify multiple elements at the first depth!"); - } - - $outerMostOperation = (array_keys($conditions))[0]; - $outerMostOperation = (is_string($outerMostOperation)) ? strtolower($outerMostOperation) : $outerMostOperation; - - $allowedOuterMostOperations = [0, "and", "or"]; - - if(!in_array($outerMostOperation, $allowedOuterMostOperations, true)){ - throw new InvalidArgumentException("Outer most operation has to one of the following: ( 0 / and / or ) "); - } - - $this->nestedWhere = $conditions; - - return $this; - } - -} diff --git a/core/class/sleekDB/SleekDB.php b/core/class/sleekDB/SleekDB.php deleted file mode 100644 index 76ef66eb..00000000 --- a/core/class/sleekDB/SleekDB.php +++ /dev/null @@ -1,578 +0,0 @@ -init($storeName, $dataDir, $configuration); - } - - /** - * Initialize the SleekDB instance. - * @param string $storeName - * @param string $dataDir - * @param array $conf - * @throws InvalidArgumentException - * @throws IOException - * @throws InvalidConfigurationException - */ - public function init(string $storeName, string $dataDir, array $conf = []){ - $this->setStore(new Store($storeName, $dataDir, $conf)); - $this->setQueryBuilder($this->getStore()->createQueryBuilder()); - } - - /** - * Initialize the store. - * @param string $storeName - * @param string $dataDir - * @param array $configuration - * @return SleekDB - * @throws InvalidArgumentException - * @throws IOException - * @throws InvalidConfigurationException - */ - public static function store(string $storeName, string $dataDir, array $configuration = []): SleekDB - { - return new SleekDB($storeName, $dataDir, $configuration); - } - - /** - * Execute Query and get Results - * @return array - * @throws InvalidArgumentException - * @throws IOException - */ - public function fetch(): array - { - return $this->getQuery()->fetch(); - } - - /** - * Check if data is found - * @return bool - * @throws InvalidArgumentException - * @throws IOException - */ - public function exists(): bool - { - return $this->getQuery()->exists(); - } - - /** - * Return the first document. - * @return array - * @throws InvalidArgumentException - * @throws IOException - */ - public function first(): array - { - return $this->getQuery()->first(); - } - - /** - * Creates a new object in the store. - * It is stored as a plaintext JSON document. - * @param array $storeData - * @return array - * @throws IOException - * @throws IdNotAllowedException - * @throws InvalidArgumentException - * @throws JsonException - */ - public function insert(array $storeData): array - { - return $this->getStore()->insert($storeData); - } - - /** - * Creates multiple objects in the store. - * @param array $storeData - * @return array - * @throws IOException - * @throws IdNotAllowedException - * @throws InvalidArgumentException - * @throws JsonException - */ - public function insertMany(array $storeData): array - { - return $this->getStore()->insertMany($storeData); - } - - /** - * Update one or multiple documents, based on current query - * @param array $updatable - * @return bool - * @throws InvalidArgumentException - * @throws IOException - */ - public function update(array $updatable): bool - { - return $this->getQuery()->update($updatable); - } - - /** - * Deletes matched store objects. - * @param int $returnOption - * @return bool|array|int - * @throws InvalidArgumentException - * @throws IOException - */ - public function delete(int $returnOption = Query::DELETE_RETURN_BOOL){ - return $this->getQuery()->delete($returnOption); - } - - /** - * Deletes a store and wipes all the data and cache it contains. - * @return bool - * @throws IOException - */ - public function deleteStore(): bool - { - return $this->getStore()->deleteStore(); - } - - /** - * This method would make a unique token for the current query. - * We would use this hash token as the id/name of the cache file. - * @return string - */ - public function getCacheToken(): string - { - return $this->getQueryBuilder()->getQuery()->getCache()->getToken(); - } - - /** - * Select specific fields - * @param string[] $fieldNames - * @return SleekDB - */ - public function select(array $fieldNames): SleekDB - { - $this->setQueryBuilder($this->getQueryBuilder()->select($fieldNames)); - return $this; - } - - /** - * Exclude specific fields - * @param string[] $fieldNames - * @return SleekDB - * @throws InvalidArgumentException - */ - public function except(array $fieldNames): SleekDB - { - $this->setQueryBuilder($this->getQueryBuilder()->except($fieldNames)); - return $this; - } - - /** - * Add conditions to filter data. - * @param string|array|mixed ...$conditions (string fieldName, string condition, mixed value) OR (array(array(string fieldName, string condition, mixed value)[, array(...)])) - * @return SleekDB - * @throws InvalidArgumentException - */ - public function where(...$conditions): SleekDB - { - foreach ($conditions as $key => $arg) { - if ($key > 0) { - throw new InvalidArgumentException("Allowed: (string fieldName, string condition, mixed value) OR (array(array(string fieldName, string condition, mixed value)[, array(...)]))"); - } - if (is_array($arg)) { - // parameters given as arrays for multiple "where" with "and" between each condition - $this->setQueryBuilder($this->getQueryBuilder()->where($arg)); - break; - } - if (count($conditions) === 3) { - // parameters given as (string fieldName, string condition, mixed value) for a single "where" - $this->setQueryBuilder($this->getQueryBuilder()->where($conditions)); - break; - } - } - - return $this; - } - - /** - * Add "in" condition to filter data. - * @param string $fieldName - * @param array $values - * @return SleekDB - * @throws InvalidArgumentException - */ - public function in(string $fieldName, array $values = []): SleekDB - { - $this->setQueryBuilder($this->getQueryBuilder()->in($fieldName, $values)); - return $this; - } - - /** - * Add "not in" condition to filter data. - * @param string $fieldName - * @param array $values - * @return SleekDB - * @throws InvalidArgumentException - */ - public function notIn(string $fieldName, array $values = []): SleekDB - { - $this->setQueryBuilder($this->getQueryBuilder()->notIn($fieldName, $values)); - return $this; - } - - /** - * Add or-where conditions to filter data. - * @param string|array|mixed ...$conditions (string fieldName, string condition, mixed value) OR array(array(string fieldName, string condition, mixed value) [, array(...)]) - * @return SleekDB - * @throws InvalidArgumentException - */ - public function orWhere(...$conditions): SleekDB - { - foreach ($conditions as $key => $arg) { - if ($key > 0) { - throw new InvalidArgumentException("Allowed: (string fieldName, string condition, mixed value) OR array(array(string fieldName, string condition, mixed value) [, array(...)])"); - } - if (is_array($arg)) { - // parameters given as arrays for an "or where" with "and" between each condition - $this->setQueryBuilder($this->getQueryBuilder()->orWhere($arg)); - break; - } - if (count($conditions) === 3) { - // parameters given as (string fieldName, string condition, mixed value) for a single "or where" - $this->setQueryBuilder($this->getQueryBuilder()->orWhere($conditions)); - break; - } - } - - return $this; - } - - /** - * Set the amount of data record to skip. - * @param int $skip - * @return SleekDB - * @throws InvalidArgumentException - */ - public function skip(int $skip = 0): SleekDB - { - $this->setQueryBuilder($this->getQueryBuilder()->skip($skip)); - return $this; - } - - /** - * Set the amount of data record to limit. - * @param int $limit - * @return SleekDB - * @throws InvalidArgumentException - */ - public function limit(int $limit = 0): SleekDB - { - $this->setQueryBuilder($this->getQueryBuilder()->limit($limit)); - return $this; - } - - /** - * Set the sort order. - * @param string $order "asc" or "desc" - * @param string $orderBy - * @return SleekDB - * @throws InvalidArgumentException - */ - public function orderBy(string $order, string $orderBy = '_id'): SleekDB - { - $this->setQueryBuilder($this->getQueryBuilder()->orderBy([$orderBy => $order])); - return $this; - } - - /** - * Do a fulltext like search against more than one field. - * @param string|array $field one fieldName or multiple fieldNames as an array - * @param string $keyword - * @return SleekDB - * @throws InvalidArgumentException - */ - public function search($field, string $keyword): SleekDB - { - $this->setQueryBuilder($this->getQueryBuilder()->search($field, $keyword)); - return $this; - } - - /** - * @param Closure $joinedStore - * @param string $dataPropertyName - * @return SleekDB - */ - public function join(Closure $joinedStore, string $dataPropertyName): SleekDB - { - $this->setQueryBuilder($this->getQueryBuilder()->join($joinedStore, $dataPropertyName)); - return $this; - } - - /** - * Re-generate the cache for the query. - * @return SleekDB - */ - public function makeCache(): SleekDB - { - $this->setQueryBuilder($this->getQueryBuilder()->regenerateCache()); - return $this; - } - - /** - * Disable cache for the query. - * @return SleekDB - */ - public function disableCache(): SleekDB - { - $this->setQueryBuilder($this->getQueryBuilder()->disableCache()); - return $this; - } - - /** - * Use caching for current query - * @param int|null $lifetime time to live as int in seconds or null to regenerate cache on every insert, update and delete - * @return SleekDB - * @throws InvalidArgumentException - */ - public function useCache(int $lifetime = null): SleekDB - { - $this->setQueryBuilder($this->getQueryBuilder()->useCache($lifetime)); - return $this; - } - - /** - * Delete cache file/s for current query. - * @return SleekDB - */ - public function deleteCache(): SleekDB - { - $this->getQueryBuilder()->getQuery()->getCache()->delete(); - return $this; - } - - /** - * Delete all cache files for current store. - * @return SleekDB - */ - public function deleteAllCache(): SleekDB - { - $this->getQueryBuilder()->getQuery()->getCache()->deleteAll(); - return $this; - } - - /** - * Keep the active query conditions. - * @return SleekDB - */ - public function keepConditions(): SleekDB - { - $this->shouldKeepConditions = true; - return $this; - } - - /** - * Return distinct values. - * @param array|string $fields - * @return SleekDB - * @throws InvalidArgumentException - */ - public function distinct($fields = []): SleekDB - { - $this->setQueryBuilder($this->getQueryBuilder()->distinct($fields)); - return $this; - } - - /** - * @return QueryBuilder - */ - public function getQueryBuilder(): QueryBuilder - { - return $this->queryBuilder; - } - - /** - * @param QueryBuilder $queryBuilder - */ - private function setQueryBuilder(QueryBuilder $queryBuilder){ - $this->queryBuilder = $queryBuilder; - } - - /** - * @return Query - */ - public function getQuery(): Query - { - $query = $this->getQueryBuilder()->getQuery(); - $this->resetQueryBuilder(); - return $query; - } - - /** - * @return Cache - */ - public function getCache(): Cache - { - // we do not want to reset the QueryBuilder - return $this->getQueryBuilder()->getQuery()->getCache(); - } - - - /** - * @param Store $store - */ - private function setStore(Store $store){ - $this->store = $store; - } - - /** - * @return Store - */ - public function getStore(): Store - { - return $this->store; - } - - /** - * Handle shouldKeepConditions and reset queryBuilder accordingly - */ - private function resetQueryBuilder(){ - if($this->shouldKeepConditions === true) { - return; - } - $this->setQueryBuilder($this->getStore()->createQueryBuilder()); - } - - - /** - * Retrieve all documents. - * @return array - * @throws IOException - * @throws InvalidArgumentException - */ - public function findAll(): array - { - return $this->getStore()->findAll(); - } - - /** - * Retrieve one document by its _id. Very fast because it finds the document by its file path. - * @param int $id - * @return array|null - * @throws InvalidArgumentException - */ - public function findById(int $id){ - return $this->getStore()->findById($id); - } - - /** - * Retrieve one or multiple documents. - * @param array $criteria - * @param array $orderBy - * @param int $limit - * @param int $offset - * @return array - * @throws IOException - * @throws InvalidArgumentException - */ - public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array - { - return $this->getStore()->findBy($criteria, $orderBy, $limit, $offset); - } - - /** - * Retrieve one document. - * @param array $criteria - * @return array|null single document or NULL if no document can be found - * @throws IOException - * @throws InvalidArgumentException - */ - public function findOneBy(array $criteria) - { - return $this->getStore()->findOneBy($criteria); - } - - /** - * Update one or multiple documents. - * @param array $updatable true if all documents could be updated and false if one document did not exist - * @return bool - * @throws IOException - * @throws InvalidArgumentException - */ - public function updateBy(array $updatable): bool - { - return $this->getStore()->update($updatable); - } - - /** - * Delete one or multiple documents. - * @param $criteria - * @param int $returnOption - * @return array|bool|int - * @throws IOException - * @throws InvalidArgumentException - */ - public function deleteBy($criteria, $returnOption = Query::DELETE_RETURN_BOOL){ - return $this->getStore()->deleteBy($criteria, $returnOption); - } - - /** - * Delete one document by its _id. Very fast because it deletes the document by its file path. - * @param $id - * @return bool true if document does not exist or deletion was successful, false otherwise - * @throws IOException - */ - public function deleteById($id): bool - { - return $this->getStore()->deleteById($id); - } - - /** - * Add a where statement that is nested. ( $x or ($y and $z) ) - * @param array $conditions - * @return $this - * @throws InvalidArgumentException - * @deprecated since version 2.3, use where and orWhere instead. - */ - public function nestedWhere(array $conditions): SleekDB - { - $this->setQueryBuilder($this->getQueryBuilder()->nestedWhere($conditions)); - return $this; - } - -} diff --git a/core/class/sleekDB/Store.php b/core/class/sleekDB/Store.php deleted file mode 100644 index 9ce2492c..00000000 --- a/core/class/sleekDB/Store.php +++ /dev/null @@ -1,888 +0,0 @@ - 2, - "scoreKey" => "searchScore", - "mode" => "or", - "algorithm" => Query::SEARCH_ALGORITHM["hits"] - ]; - - const dataDirectory = "data/"; - - /** - * Store constructor. - * @param string $storeName - * @param string $databasePath - * @param array $configuration - * @throws InvalidArgumentException - * @throws IOException - * @throws InvalidConfigurationException - */ - public function __construct(string $storeName, string $databasePath, array $configuration = []) - { - $storeName = trim($storeName); - if (empty($storeName)) { - throw new InvalidArgumentException('store name can not be empty'); - } - $this->storeName = $storeName; - - $databasePath = trim($databasePath); - if (empty($databasePath)) { - throw new InvalidArgumentException('data directory can not be empty'); - } - - IoHelper::normalizeDirectory($databasePath); - $this->databasePath = $databasePath; - - $this->setConfiguration($configuration); - - // boot store - $this->createDatabasePath(); - $this->createStore(); - } - - /** - * Change the destination of the store object. - * @param string $storeName - * @param string|null $databasePath If empty, previous database path will be used. - * @param array $configuration - * @return Store - * @throws IOException - * @throws InvalidArgumentException - * @throws InvalidConfigurationException - */ - public function changeStore(string $storeName, string $databasePath = null, array $configuration = []): Store - { - if(empty($databasePath)){ - $databasePath = $this->getDatabasePath(); - } - $this->__construct($storeName, $databasePath, $configuration); - return $this; - } - - /** - * @return string - */ - public function getStoreName(): string - { - return $this->storeName; - } - - /** - * @return string - */ - public function getDatabasePath(): string - { - return $this->databasePath; - } - - /** - * @return QueryBuilder - */ - public function createQueryBuilder(): QueryBuilder - { - return new QueryBuilder($this); - } - - /** - * Insert a new document to the store. - * It is stored as a plaintext JSON document. - * @param array $data - * @return array inserted document - * @throws IOException - * @throws IdNotAllowedException - * @throws InvalidArgumentException - * @throws JsonException - */ - public function insert(array $data): array - { - // Handle invalid data - if (empty($data)) { - throw new InvalidArgumentException('No data found to insert in the store'); - } - - $data = $this->writeNewDocumentToStore($data); - - $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime(); - - return $data; - } - - /** - * Insert multiple documents to the store. - * They are stored as plaintext JSON documents. - * @param array $data - * @return array inserted documents - * @throws IOException - * @throws IdNotAllowedException - * @throws InvalidArgumentException - * @throws JsonException - */ - public function insertMany(array $data): array - { - // Handle invalid data - if (empty($data)) { - throw new InvalidArgumentException('No data found to insert in the store'); - } - - // All results. - $results = []; - foreach ($data as $document) { - $results[] = $this->writeNewDocumentToStore($document); - } - - $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime(); - return $results; - } - - - /** - * Delete store with all its data and cache. - * @return bool - * @throws IOException - */ - public function deleteStore(): bool - { - $storePath = $this->getStorePath(); - return IoHelper::deleteFolder($storePath); - } - - - /** - * Return the last created store object ID. - * @return int - * @throws IOException - */ - public function getLastInsertedId(): int - { - $counterPath = $this->getStorePath() . '_cnt.sdb'; - - return (int) IoHelper::getFileContent($counterPath); - } - - /** - * @return string - */ - public function getStorePath(): string - { - return $this->storePath; - } - - /** - * Retrieve all documents. - * @param array|null $orderBy array($fieldName => $order). $order can be "asc" or "desc" - * @param int|null $limit the amount of data record to limit - * @param int|null $offset the amount of data record to skip - * @return array - * @throws IOException - * @throws InvalidArgumentException - */ - public function findAll(array $orderBy = null, int $limit = null, int $offset = null): array - { - $qb = $this->createQueryBuilder(); - if(!is_null($orderBy)){ - $qb->orderBy($orderBy); - } - if(!is_null($limit)){ - $qb->limit($limit); - } - if(!is_null($offset)){ - $qb->skip($offset); - } - return $qb->getQuery()->fetch(); - } - - /** - * Retrieve one document by its primary key. Very fast because it finds the document by its file path. - * @param int|string $id - * @return array|null - * @throws InvalidArgumentException - */ - public function findById($id){ - - $id = $this->checkAndStripId($id); - - $filePath = $this->getDataPath() . "$id.json"; - - try{ - $content = IoHelper::getFileContent($filePath); - } catch (Exception $exception){ - return null; - } - - return @json_decode($content, true); - } - - /** - * Retrieve one or multiple documents. - * @param array $criteria - * @param array $orderBy - * @param int $limit - * @param int $offset - * @return array - * @throws IOException - * @throws InvalidArgumentException - */ - public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array - { - $qb = $this->createQueryBuilder(); - - $qb->where($criteria); - - if($orderBy !== null) { - $qb->orderBy($orderBy); - } - if($limit !== null) { - $qb->limit($limit); - } - if($offset !== null) { - $qb->skip($offset); - } - - return $qb->getQuery()->fetch(); - } - - /** - * Retrieve one document. - * @param array $criteria - * @return array|null single document or NULL if no document can be found - * @throws IOException - * @throws InvalidArgumentException - */ - public function findOneBy(array $criteria) - { - $qb = $this->createQueryBuilder(); - - $qb->where($criteria); - - $result = $qb->getQuery()->first(); - - return (!empty($result))? $result : null; - - } - - /** - * Update or insert one document. - * @param array $data - * @param bool $autoGenerateIdOnInsert - * @return array updated / inserted document - * @throws IOException - * @throws InvalidArgumentException - * @throws JsonException - */ - public function updateOrInsert(array $data, bool $autoGenerateIdOnInsert = true): array - { - $primaryKey = $this->getPrimaryKey(); - - if(empty($data)) { - throw new InvalidArgumentException("No document to update or insert."); - } - -// // we can use this check to determine if multiple documents are given -// // because documents have to have at least the primary key. -// if(array_keys($data) !== range(0, (count($data) - 1))){ -// $data = [ $data ]; -// } - - if(!array_key_exists($primaryKey, $data)) { -// $documentString = var_export($document, true); -// throw new InvalidArgumentException("Documents have to have the primary key \"$primaryKey\". Got data: $documentString"); - $data[$primaryKey] = $this->increaseCounterAndGetNextId(); - } else { - $data[$primaryKey] = $this->checkAndStripId($data[$primaryKey]); - if($autoGenerateIdOnInsert && $this->findById($data[$primaryKey]) === null){ - $data[$primaryKey] = $this->increaseCounterAndGetNextId(); - } - } - - // One document to update or insert - - // save to access file with primary key value because we secured it above - $storePath = $this->getDataPath() . "$data[$primaryKey].json"; - IoHelper::writeContentToFile($storePath, json_encode($data)); - - $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime(); - - return $data; - } - - /** - * Update or insert multiple documents. - * @param array $data - * @param bool $autoGenerateIdOnInsert - * @return array updated / inserted documents - * @throws IOException - * @throws InvalidArgumentException - * @throws JsonException - */ - public function updateOrInsertMany(array $data, bool $autoGenerateIdOnInsert = true): array - { - $primaryKey = $this->getPrimaryKey(); - - if(empty($data)) { - throw new InvalidArgumentException("No documents to update or insert."); - } - -// // we can use this check to determine if multiple documents are given -// // because documents have to have at least the primary key. -// if(array_keys($data) !== range(0, (count($data) - 1))){ -// $data = [ $data ]; -// } - - // Check if all documents have the primary key before updating or inserting any - foreach ($data as $key => $document){ - if(!is_array($document)) { - throw new InvalidArgumentException('Documents have to be an arrays.'); - } - if(!array_key_exists($primaryKey, $document)) { -// $documentString = var_export($document, true); -// throw new InvalidArgumentException("Documents have to have the primary key \"$primaryKey\". Got data: $documentString"); - $document[$primaryKey] = $this->increaseCounterAndGetNextId(); - } else { - $document[$primaryKey] = $this->checkAndStripId($document[$primaryKey]); - if($autoGenerateIdOnInsert && $this->findById($document[$primaryKey]) === null){ - $document[$primaryKey] = $this->increaseCounterAndGetNextId(); - } - } - // after the stripping and checking we apply it back - $data[$key] = $document; - } - - // One or multiple documents to update or insert - foreach ($data as $document) { - // save to access file with primary key value because we secured it above - $storePath = $this->getDataPath() . "$document[$primaryKey].json"; - IoHelper::writeContentToFile($storePath, json_encode($document)); - } - - $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime(); - - return $data; - } - - - /** - * Update one or multiple documents. - * @param array $updatable - * @return bool true if all documents could be updated and false if one document did not exist - * @throws IOException - * @throws InvalidArgumentException - */ - public function update(array $updatable): bool - { - $primaryKey = $this->getPrimaryKey(); - - if(empty($updatable)) { - throw new InvalidArgumentException("No documents to update."); - } - - // we can use this check to determine if multiple documents are given - // because documents have to have at least the primary key. - if(array_keys($updatable) !== range(0, (count($updatable) - 1))){ - $updatable = [ $updatable ]; - } - - // Check if all documents exist and have the primary key before updating any - foreach ($updatable as $key => $document){ - if(!is_array($document)) { - throw new InvalidArgumentException('Documents have to be an arrays.'); - } - if(!array_key_exists($primaryKey, $document)) { - throw new InvalidArgumentException("Documents have to have the primary key \"$primaryKey\"."); - } - - $document[$primaryKey] = $this->checkAndStripId($document[$primaryKey]); - // after the stripping and checking we apply it back to the updatable array. - $updatable[$key] = $document; - - $storePath = $this->getDataPath() . "$document[$primaryKey].json"; - - if (!file_exists($storePath)) { - return false; - } - } - - // One or multiple documents to update - foreach ($updatable as $document) { - // save to access file with primary key value because we secured it above - $storePath = $this->getDataPath() . "$document[$primaryKey].json"; - IoHelper::writeContentToFile($storePath, json_encode($document)); - } - - $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime(); - - return true; - } - - /** - * Update properties of one document. - * @param int|string $id - * @param array $updatable - * @return array|false Updated document or false if document does not exist. - * @throws IOException If document could not be read or written. - * @throws InvalidArgumentException If one key to update is primary key or $id is not int or string. - * @throws JsonException If content of document file could not be decoded. - */ - public function updateById($id, array $updatable) - { - - $id = $this->checkAndStripId($id); - - $filePath = $this->getDataPath() . "$id.json"; - - $primaryKey = $this->getPrimaryKey(); - - if(array_key_exists($primaryKey, $updatable)) { - throw new InvalidArgumentException("You can not update the primary key \"$primaryKey\" of documents."); - } - - if(!file_exists($filePath)){ - return false; - } - - $content = IoHelper::updateFileContent($filePath, function($content) use ($filePath, $updatable){ - $content = @json_decode($content, true); - if(!is_array($content)){ - throw new JsonException("Could not decode content of \"$filePath\" with json_decode."); - } - foreach ($updatable as $key => $value){ - NestedHelper::updateNestedValue($key, $content, $value); - } - return json_encode($content); - }); - - $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime(); - - return json_decode($content, true); - } - - /** - * Delete one or multiple documents. - * @param array $criteria - * @param int $returnOption - * @return array|bool|int - * @throws IOException - * @throws InvalidArgumentException - */ - public function deleteBy(array $criteria, int $returnOption = Query::DELETE_RETURN_BOOL){ - - $query = $this->createQueryBuilder()->where($criteria)->getQuery(); - - $query->getCache()->deleteAllWithNoLifetime(); - - return $query->delete($returnOption); - } - - /** - * Delete one document by its primary key. Very fast because it deletes the document by its file path. - * @param int|string $id - * @return bool true if document does not exist or deletion was successful, false otherwise - * @throws InvalidArgumentException - */ - public function deleteById($id): bool - { - - $id = $this->checkAndStripId($id); - - $filePath = $this->getDataPath() . "$id.json"; - - $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime(); - - return (!file_exists($filePath) || true === @unlink($filePath)); - } - - /** - * Remove fields from one document by its primary key. - * @param int|string $id - * @param array $fieldsToRemove - * @return false|array - * @throws IOException - * @throws InvalidArgumentException - * @throws JsonException - */ - public function removeFieldsById($id, array $fieldsToRemove) - { - $id = $this->checkAndStripId($id); - $filePath = $this->getDataPath() . "$id.json"; - $primaryKey = $this->getPrimaryKey(); - - if(in_array($primaryKey, $fieldsToRemove, false)) { - throw new InvalidArgumentException("You can not remove the primary key \"$primaryKey\" of documents."); - } - if(!file_exists($filePath)){ - return false; - } - - $content = IoHelper::updateFileContent($filePath, function($content) use ($filePath, $fieldsToRemove){ - $content = @json_decode($content, true); - if(!is_array($content)){ - throw new JsonException("Could not decode content of \"$filePath\" with json_decode."); - } - foreach ($fieldsToRemove as $fieldToRemove){ - NestedHelper::removeNestedField($content, $fieldToRemove); - } - return $content; - }); - - $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime(); - - return json_decode($content, true); - } - - /** - * Do a fulltext like search against one or multiple fields. - * @param array $fields - * @param string $query - * @param array|null $orderBy - * @param int|null $limit - * @param int|null $offset - * @return array - * @throws IOException - * @throws InvalidArgumentException - */ - public function search(array $fields, string $query, array $orderBy = null, int $limit = null, int $offset = null): array - { - - $qb = $this->createQueryBuilder(); - - $qb->search($fields, $query); - - if($orderBy !== null) { - $qb->orderBy($orderBy); - } - - if($limit !== null) { - $qb->limit($limit); - } - - if($offset !== null) { - $qb->skip($offset); - } - - return $qb->getQuery()->fetch(); - } - - /** - * Get the name of the field used as the primary key. - * @return string - */ - public function getPrimaryKey(): string - { - return $this->primaryKey; - } - - /** - * Returns the amount of documents in the store. - * @return int - * @throws IOException - */ - public function count(): int - { - if($this->_getUseCache() === true){ - $cacheTokenArray = ["count" => true]; - $cache = new Cache($this->getStorePath(), $cacheTokenArray, null); - $cacheValue = $cache->get(); - if(is_array($cacheValue) && array_key_exists("count", $cacheValue)){ - return $cacheValue["count"]; - } - } - $value = [ - "count" => IoHelper::countFolderContent($this->getDataPath()) - ]; - if(isset($cache)) { - $cache->set($value); - } - return $value["count"]; - } - - /** - * Returns the search options of the store. - * @return array - */ - public function _getSearchOptions(): array - { - return $this->searchOptions; - } - - /** - * Returns if caching is enabled store wide. - * @return bool - */ - public function _getUseCache(): bool - { - return $this->useCache; - } - - /** - * Returns the store wide default cache lifetime. - * @return null|int - */ - public function _getDefaultCacheLifetime() - { - return $this->defaultCacheLifetime; - } - - /** - * @return string - * @deprecated since version 2.7, use getDatabasePath instead. - */ - public function getDataDirectory(): string - { - // TODO remove with version 3.0 - return $this->databasePath; - } - - /** - * @throws IOException - */ - private function createDatabasePath() - { - $databasePath = $this->getDatabasePath(); - IoHelper::createFolder($databasePath); - } - - /** - * @throws IOException - */ - private function createStore() - { - $storeName = $this->getStoreName(); - // Prepare store name. - IoHelper::normalizeDirectory($storeName); - // Store directory path. - $this->storePath = $this->getDatabasePath() . $storeName; - $storePath = $this->getStorePath(); - IoHelper::createFolder($storePath); - - // Create the cache directory. - $cacheDirectory = $storePath . 'cache'; - IoHelper::createFolder($cacheDirectory); - - // Create the data directory. - IoHelper::createFolder($storePath . self::dataDirectory); - - // Create the store counter file. - $counterFile = $storePath . '_cnt.sdb'; - if(!file_exists($counterFile)){ - IoHelper::writeContentToFile($counterFile, '0'); - } - } - - /** - * @param array $configuration - * @throws InvalidConfigurationException - */ - private function setConfiguration(array $configuration) - { - if(array_key_exists("auto_cache", $configuration)){ - $autoCache = $configuration["auto_cache"]; - if(!is_bool($configuration["auto_cache"])){ - throw new InvalidConfigurationException("auto_cache has to be boolean"); - } - - $this->useCache = $autoCache; - } - - if(array_key_exists("cache_lifetime", $configuration)){ - $defaultCacheLifetime = $configuration["cache_lifetime"]; - if(!is_int($defaultCacheLifetime) && !is_null($defaultCacheLifetime)){ - throw new InvalidConfigurationException("cache_lifetime has to be null or int"); - } - - $this->defaultCacheLifetime = $defaultCacheLifetime; - } - - // TODO remove timeout on major update - // Set timeout. - if (array_key_exists("timeout", $configuration)) { - if ((!is_int($configuration['timeout']) || $configuration['timeout'] <= 0) && !($configuration['timeout'] === false)){ - throw new InvalidConfigurationException("timeout has to be an int > 0 or false"); - } - $this->timeout = $configuration["timeout"]; - } - if($this->timeout !== false){ - $message = 'The "timeout" configuration is deprecated and will be removed with the next major update.' . - ' Set the "timeout" configuration to false and if needed use the set_timeout_limit() function in your own code.'; - trigger_error($message, E_USER_DEPRECATED); - set_time_limit($this->timeout); - } - - if(array_key_exists("primary_key", $configuration)){ - $primaryKey = $configuration["primary_key"]; - if(!is_string($primaryKey)){ - throw new InvalidConfigurationException("primary key has to be a string"); - } - $this->primaryKey = $primaryKey; - } - - if(array_key_exists("search", $configuration)){ - $searchConfig = $configuration["search"]; - - if(array_key_exists("min_length", $searchConfig)){ - $searchMinLength = $searchConfig["min_length"]; - if(!is_int($searchMinLength) || $searchMinLength <= 0){ - throw new InvalidConfigurationException("min length for searching has to be an int >= 0"); - } - $this->searchOptions["minLength"] = $searchMinLength; - } - - if(array_key_exists("mode", $searchConfig)){ - $searchMode = $searchConfig["mode"]; - if(!is_string($searchMode) || !in_array(strtolower(trim($searchMode)), ["and", "or"])){ - throw new InvalidConfigurationException("search mode can just be \"and\" or \"or\""); - } - $this->searchOptions["mode"] = strtolower(trim($searchMode)); - } - - if(array_key_exists("score_key", $searchConfig)){ - $searchScoreKey = $searchConfig["score_key"]; - if((!is_string($searchScoreKey) && !is_null($searchScoreKey))){ - throw new InvalidConfigurationException("search score key for search has to be a not empty string or null"); - } - $this->searchOptions["scoreKey"] = $searchScoreKey; - } - - if(array_key_exists("algorithm", $searchConfig)){ - $searchAlgorithm = $searchConfig["algorithm"]; - if(!in_array($searchAlgorithm, Query::SEARCH_ALGORITHM, true)){ - $searchAlgorithm = implode(', ', $searchAlgorithm); - throw new InvalidConfigurationException("The search algorithm has to be one of the following integer values ($searchAlgorithm)"); - } - $this->searchOptions["algorithm"] = $searchAlgorithm; - } - } - } - - /** - * Writes an object in a store. - * @param array $storeData - * @return array - * @throws IOException - * @throws IdNotAllowedException - * @throws JsonException - */ - private function writeNewDocumentToStore(array $storeData): array - { - $primaryKey = $this->getPrimaryKey(); - // Check if it has the primary key - if (isset($storeData[$primaryKey])) { - throw new IdNotAllowedException( - "The \"$primaryKey\" index is reserved by SleekDB, please delete the $primaryKey key and try again" - ); - } - $id = $this->increaseCounterAndGetNextId(); - // Add the system ID with the store data array. - $storeData[$primaryKey] = $id; - // Prepare storable data - $storableJSON = @json_encode($storeData); - if ($storableJSON === false) { - throw new JsonException('Unable to encode the data array, - please provide a valid PHP associative array'); - } - // Define the store path - $filePath = $this->getDataPath()."$id.json"; - - IoHelper::writeContentToFile($filePath, $storableJSON); - - return $storeData; - } - - /** - * Increments the store wide unique store object ID and returns it. - * @return int - * @throws IOException - * @throws JsonException - */ - private function increaseCounterAndGetNextId(): int - { - $counterPath = $this->getStorePath() . '_cnt.sdb'; - - if (!file_exists($counterPath)) { - throw new IOException("File $counterPath does not exist."); - } - - $dataPath = $this->getDataPath(); - - return (int) IoHelper::updateFileContent($counterPath, function ($counter) use ($dataPath){ - $newCounter = ((int) $counter) + 1; - - while(file_exists($dataPath."$newCounter.json") === true){ - $newCounter++; - } - return (string)$newCounter; - }); - } - - - /** - * @param string|int $id - * @return int - * @throws InvalidArgumentException - */ - private function checkAndStripId($id): int - { - if(!is_string($id) && !is_int($id)){ - throw new InvalidArgumentException("The id of the document has to be an integer or string"); - } - - if(is_string($id)){ - $id = IoHelper::secureStringForFileAccess($id); - } - - if(!is_numeric($id)){ - throw new InvalidArgumentException("The id of the document has to be numeric"); - } - - return (int) $id; - } - - /** - * @return string - */ - private function getDataPath(): string - { - return $this->getStorePath() . self::dataDirectory; - } - -}