-
+
+ = $_SERVER["HTTP_REMOTE_NAME"] ?? "?" ?>
diff --git a/_/libs/sleekdb/Cache.php b/_/libs/sleekdb/Cache.php
new file mode 100644
index 0000000..806cb2a
--- /dev/null
+++ b/_/libs/sleekdb/Cache.php
@@ -0,0 +1,309 @@
+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/_/libs/sleekdb/Classes/CacheHandler.php b/_/libs/sleekdb/Classes/CacheHandler.php
new file mode 100644
index 0000000..2104d9f
--- /dev/null
+++ b/_/libs/sleekdb/Classes/CacheHandler.php
@@ -0,0 +1,129 @@
+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/_/libs/sleekdb/Classes/ConditionsHandler.php b/_/libs/sleekdb/Classes/ConditionsHandler.php
new file mode 100644
index 0000000..6a4f21f
--- /dev/null
+++ b/_/libs/sleekdb/Classes/ConditionsHandler.php
@@ -0,0 +1,458 @@
+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 = array(
+ '\\' => '\\\\', // Escape backslash to prevent unwanted behaviour
+ '/' => '\/', // slash needs to be escaped because it's used as delimiter
+ '.' => '\.',
+ '+' => '\+',
+ '*' => '\*',
+ '?' => '\?',
+ '$' => '\$',
+ '(' => '\(',
+ ')' => '\)',
+ '{' => '\{',
+ '}' => '\}',
+ '=' => '\=',
+ '!' => '\!',
+ '<' => '\<',
+ '>' => '\>',
+ '|' => '\|',
+ ':' => '\:',
+ '#' => '\#',
+ '%' => '.*',
+ '_' => '.{1}',
+ // Allow escaping of % and _ with backslash
+ '\.*' => '%',
+ '\.{1}' => '_',
+ // Allow escaping of wildcards
+ '\\\\[' => '\[',
+ '\\\\^' => '\^',
+ '\\\\]' => '\]',
+ '\\\\-' => '\-',
+ );
+ $value = str_replace(array_keys($charactersToEscape), array_values($charactersToEscape), $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/_/libs/sleekdb/Classes/DocumentFinder.php b/_/libs/sleekdb/Classes/DocumentFinder.php
new file mode 100644
index 0000000..d201496
--- /dev/null
+++ b/_/libs/sleekdb/Classes/DocumentFinder.php
@@ -0,0 +1,387 @@
+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/_/libs/sleekdb/Classes/DocumentReducer.php b/_/libs/sleekdb/Classes/DocumentReducer.php
new file mode 100644
index 0000000..d861566
--- /dev/null
+++ b/_/libs/sleekdb/Classes/DocumentReducer.php
@@ -0,0 +1,665 @@
+ "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/_/libs/sleekdb/Classes/DocumentUpdater.php b/_/libs/sleekdb/Classes/DocumentUpdater.php
new file mode 100644
index 0000000..20fd98c
--- /dev/null
+++ b/_/libs/sleekdb/Classes/DocumentUpdater.php
@@ -0,0 +1,159 @@
+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/_/libs/sleekdb/Classes/IoHelper.php b/_/libs/sleekdb/Classes/IoHelper.php
new file mode 100644
index 0000000..9a2d8ed
--- /dev/null
+++ b/_/libs/sleekdb/Classes/IoHelper.php
@@ -0,0 +1,252 @@
+isDir()) {
+ rmdir($file->getRealPath());
+ } else {
+ unlink($file->getRealPath());
+ }
+ }
+ return rmdir($folderPath);
+ }
+
+ /**
+ * @param string $folderPath
+ * @param int $chmod
+ * @throws IOException
+ */
+ public static function createFolder(string $folderPath, int $chmod){
+ // 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, $chmod, 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/_/libs/sleekdb/Classes/NestedHelper.php b/_/libs/sleekdb/Classes/NestedHelper.php
new file mode 100644
index 0000000..6c1979f
--- /dev/null
+++ b/_/libs/sleekdb/Classes/NestedHelper.php
@@ -0,0 +1,140 @@
+ 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/_/libs/sleekdb/Exceptions/IOException.php b/_/libs/sleekdb/Exceptions/IOException.php
new file mode 100644
index 0000000..cb63462
--- /dev/null
+++ b/_/libs/sleekdb/Exceptions/IOException.php
@@ -0,0 +1,7 @@
+ 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/_/libs/sleekdb/QueryBuilder.php b/_/libs/sleekdb/QueryBuilder.php
new file mode 100644
index 0000000..90fbfca
--- /dev/null
+++ b/_/libs/sleekdb/QueryBuilder.php
@@ -0,0 +1,516 @@
+ 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/_/libs/sleekdb/SleekDB.php b/_/libs/sleekdb/SleekDB.php
new file mode 100644
index 0000000..23e181c
--- /dev/null
+++ b/_/libs/sleekdb/SleekDB.php
@@ -0,0 +1,585 @@
+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/_/libs/sleekdb/Store.php b/_/libs/sleekdb/Store.php
new file mode 100644
index 0000000..c5a07c6
--- /dev/null
+++ b/_/libs/sleekdb/Store.php
@@ -0,0 +1,881 @@
+ 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, $this->folderPermissions);
+ }
+
+ /**
+ * @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, $this->folderPermissions);
+
+ // Create the cache directory.
+ $cacheDirectory = $storePath . 'cache';
+ IoHelper::createFolder($cacheDirectory, $this->folderPermissions);
+
+ // Create the data directory.
+ IoHelper::createFolder($storePath . self::dataDirectory, $this->folderPermissions);
+
+ // 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;
+ }
+
+ 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;
+ }
+ }
+
+ if ( array_key_exists("folder_permissions", $configuration) ) {
+ $folderPermissions = $configuration["folder_permissions"];
+ if ( !is_int($folderPermissions) ) {
+ throw new InvalidConfigurationException("folder_permissions has to be an integer (e.g. 0777)");
+ }
+ $this->folderPermissions = $folderPermissions;
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+}