19 changed files with 0 additions and 4694 deletions
@ -1,309 +0,0 @@
|
||||
<?php |
||||
|
||||
namespace SleekDB; |
||||
|
||||
use Closure; |
||||
use Exception; |
||||
use ReflectionFunction; |
||||
use SleekDB\Classes\IoHelper; |
||||
use SleekDB\Exceptions\IOException; |
||||
|
||||
/** |
||||
* Class Cache |
||||
* Caching layer of SleekDB, handles everything regarding caching. |
||||
*/ |
||||
class Cache |
||||
{ |
||||
|
||||
const DEFAULT_CACHE_DIR = "cache/"; |
||||
const NO_LIFETIME_FILE_STRING = "no_lifetime"; |
||||
|
||||
/** |
||||
* Lifetime in seconds or deletion with deleteAll |
||||
* @var int|null |
||||
*/ |
||||
protected $lifetime; |
||||
|
||||
protected $cachePath = ""; |
||||
|
||||
protected $cacheDir = ""; |
||||
|
||||
protected $tokenArray; |
||||
|
||||
/** |
||||
* Cache constructor. |
||||
* @param string $storePath |
||||
* @param array $cacheTokenArray |
||||
* @param int|null $cacheLifetime |
||||
*/ |
||||
public function __construct(string $storePath, array &$cacheTokenArray, $cacheLifetime) |
||||
{ |
||||
// TODO make it possible to define custom cache directory. |
||||
// $cacheDir = ""; |
||||
// $this->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; |
||||
} |
||||
} |
@ -1,129 +0,0 @@
|
||||
<?php |
||||
|
||||
|
||||
namespace SleekDB\Classes; |
||||
|
||||
|
||||
use SleekDB\Cache; |
||||
use SleekDB\Exceptions\IOException; |
||||
use SleekDB\QueryBuilder; |
||||
|
||||
/** |
||||
* Class CacheHandler |
||||
* Bridge between Query and Cache |
||||
*/ |
||||
class CacheHandler |
||||
{ |
||||
/** |
||||
* @var Cache |
||||
*/ |
||||
protected $cache; |
||||
|
||||
protected $cacheTokenArray; |
||||
protected $regenerateCache; |
||||
protected $useCache; |
||||
|
||||
/** |
||||
* CacheHandler constructor. |
||||
* @param string $storePath |
||||
* @param QueryBuilder $queryBuilder |
||||
*/ |
||||
public function __construct(string $storePath, QueryBuilder $queryBuilder) |
||||
{ |
||||
$this->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; |
||||
} |
||||
|
||||
} |
@ -1,433 +0,0 @@
|
||||
<?php |
||||
|
||||
|
||||
namespace SleekDB\Classes; |
||||
|
||||
|
||||
use SleekDB\Exceptions\InvalidArgumentException; |
||||
use DateTime; |
||||
use Exception; |
||||
use Throwable; |
||||
|
||||
/** |
||||
* Class ConditionsHandler |
||||
* Handle all types of conditions to check if a document has passed. |
||||
*/ |
||||
class ConditionsHandler |
||||
{ |
||||
|
||||
/** |
||||
* Get the result of an condition. |
||||
* @param string $condition |
||||
* @param mixed $fieldValue value of current field |
||||
* @param mixed $value value to check |
||||
* @return bool |
||||
* @throws InvalidArgumentException |
||||
*/ |
||||
public static function verifyCondition(string $condition, $fieldValue, $value): bool |
||||
{ |
||||
|
||||
if($value instanceof DateTime){ |
||||
// 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; |
||||
} |
||||
$value = $value->getTimestamp(); |
||||
$fieldValue = self::convertValueToTimeStamp($fieldValue); |
||||
} |
||||
|
||||
$condition = strtolower(trim($condition)); |
||||
switch ($condition){ |
||||
case "=": |
||||
case "===": |
||||
return ($fieldValue === $value); |
||||
case "==": |
||||
return ($fieldValue == $value); |
||||
case "<>": |
||||
return ($fieldValue != $value); |
||||
case "!==": |
||||
case "!=": |
||||
return ($fieldValue !== $value); |
||||
case ">": |
||||
return ($fieldValue > $value); |
||||
case ">=": |
||||
return ($fieldValue >= $value); |
||||
case "<": |
||||
return ($fieldValue < $value); |
||||
case "<=": |
||||
return ($fieldValue <= $value); |
||||
case "not like": |
||||
case "like": |
||||
|
||||
if(!is_string($value)){ |
||||
throw new InvalidArgumentException("When using \"LIKE\" or \"NOT LIKE\" the value has to be a string."); |
||||
} |
||||
|
||||
// escape characters that are part of regular expression syntax |
||||
// https://www.php.net/manual/en/function.preg-quote.php |
||||
// We can not use preg_quote because the following characters are also wildcard characters in sql |
||||
// so we will not escape them: [ ^ ] - |
||||
$charactersToEscape = [".", "\\", "+", "*", "?", "$", "(", ")", "{", "}", "=", "!", "<", ">", "|", ":", "#"]; |
||||
foreach ($charactersToEscape as $characterToEscape){ |
||||
$value = str_replace($characterToEscape, "\\".$characterToEscape, $value); |
||||
} |
||||
|
||||
$value = str_replace(array('%', '_'), array('.*', '.{1}'), $value); // (zero or more characters) and (single character) |
||||
$pattern = "/^" . $value . "$/i"; |
||||
$result = (preg_match($pattern, $fieldValue) === 1); |
||||
return ($condition === "not like") ? !$result : $result; |
||||
|
||||
case "not in": |
||||
case "in": |
||||
if(!is_array($value)){ |
||||
$value = (!is_object($value) && !is_array($value) && !is_null($value)) ? $value : gettype($value); |
||||
throw new InvalidArgumentException("When using \"in\" and \"not in\" you have to check against an array. Got: $value"); |
||||
} |
||||
if(!empty($value)){ |
||||
(list($firstElement) = $value); |
||||
if($firstElement instanceof DateTime){ |
||||
// if the user wants to use DateTime, every element of the array has to be an DateTime object. |
||||
|
||||
// compare timestamps |
||||
|
||||
// null, false or an empty string will convert to current date and time. |
||||
// That is not what we want. |
||||
if(empty($fieldValue)){ |
||||
return false; |
||||
} |
||||
|
||||
foreach ($value as $key => $item){ |
||||
if(!($item instanceof DateTime)){ |
||||
throw new InvalidArgumentException("If one DateTime object is given in an \"IN\" or \"NOT IN\" comparison, every element has to be a DateTime object!"); |
||||
} |
||||
$value[$key] = $item->getTimestamp(); |
||||
} |
||||
|
||||
$fieldValue = self::convertValueToTimeStamp($fieldValue); |
||||
} |
||||
} |
||||
$result = in_array($fieldValue, $value, true); |
||||
return ($condition === "not in") ? !$result : $result; |
||||
case "not between": |
||||
case "between": |
||||
|
||||
if(!is_array($value) || ($valueLength = count($value)) !== 2){ |
||||
$value = (!is_object($value) && !is_array($value) && !is_null($value)) ? $value : gettype($value); |
||||
if(isset($valueLength)){ |
||||
$value .= " | Length: $valueLength"; |
||||
} |
||||
throw new InvalidArgumentException("When using \"between\" you have to check against an array with a length of 2. Got: $value"); |
||||
} |
||||
|
||||
list($startValue, $endValue) = $value; |
||||
|
||||
$result = ( |
||||
self::verifyCondition(">=", $fieldValue, $startValue) |
||||
&& self::verifyCondition("<=", $fieldValue, $endValue) |
||||
); |
||||
|
||||
return ($condition === "not between") ? !$result : $result; |
||||
case "not contains": |
||||
case "contains": |
||||
|
||||
if(!is_array($fieldValue)){ |
||||
return ($condition === "not contains"); |
||||
} |
||||
|
||||
$fieldValues = []; |
||||
|
||||
if($value instanceof DateTime){ |
||||
// compare timestamps |
||||
$value = $value->getTimestamp(); |
||||
|
||||
foreach ($fieldValue as $item){ |
||||
// null, false or an empty string will convert to current date and time. |
||||
// That is not what we want. |
||||
if(empty($item)){ |
||||
continue; |
||||
} |
||||
try{ |
||||
$fieldValues[] = self::convertValueToTimeStamp($item); |
||||
} catch (Exception $exception){ |
||||
} |
||||
} |
||||
} |
||||
|
||||
if(!empty($fieldValues)){ |
||||
$result = in_array($value, $fieldValues, true); |
||||
} else { |
||||
$result = in_array($value, $fieldValue, true); |
||||
} |
||||
|
||||
return ($condition === "not contains") ? !$result : $result; |
||||
case 'exists': |
||||
return $fieldValue === $value; |
||||
default: |
||||
throw new InvalidArgumentException("Condition \"$condition\" is not allowed."); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @param array $element condition or operation |
||||
* @param array $data |
||||
* @return bool |
||||
* @throws InvalidArgumentException |
||||
*/ |
||||
public static function handleWhereConditions(array $element, array $data): bool |
||||
{ |
||||
if(empty($element)){ |
||||
throw new InvalidArgumentException("Malformed where statement! Where statements can not contain empty arrays."); |
||||
} |
||||
if(array_keys($element) !== range(0, (count($element) - 1))){ |
||||
throw new InvalidArgumentException("Malformed where statement! Associative arrays are not allowed."); |
||||
} |
||||
// element is a where condition |
||||
if(is_string($element[0]) && is_string($element[1])){ |
||||
if(count($element) !== 3){ |
||||
throw new InvalidArgumentException("Where conditions have to be [fieldName, condition, value]"); |
||||
} |
||||
|
||||
$fieldName = $element[0]; |
||||
$condition = strtolower(trim($element[1])); |
||||
$fieldValue = ($condition === 'exists') |
||||
? NestedHelper::nestedFieldExists($fieldName, $data) |
||||
: NestedHelper::getNestedValue($fieldName, $data); |
||||
|
||||
return self::verifyCondition($condition, $fieldValue, $element[2]); |
||||
} |
||||
|
||||
// element is an array "brackets" |
||||
|
||||
// prepare results array - example: [true, "and", false] |
||||
$results = []; |
||||
foreach ($element as $value){ |
||||
if(is_array($value)){ |
||||
$results[] = self::handleWhereConditions($value, $data); |
||||
} else if (is_string($value)) { |
||||
$results[] = $value; |
||||
} else if($value instanceof \Closure){ |
||||
$result = $value($data); |
||||
if(!is_bool($result)){ |
||||
$resultType = gettype($result); |
||||
$errorMsg = "The closure in the where condition needs to return a boolean. Got: $resultType"; |
||||
throw new InvalidArgumentException($errorMsg); |
||||
} |
||||
$results[] = $result; |
||||
} else { |
||||
$value = (!is_object($value) && !is_array($value) && !is_null($value)) ? $value : gettype($value); |
||||
throw new InvalidArgumentException("Invalid nested where statement element! Expected condition or operation, got: \"$value\""); |
||||
} |
||||
} |
||||
|
||||
// first result as default value |
||||
$returnValue = array_shift($results); |
||||
|
||||
if(is_bool($returnValue) === false){ |
||||
throw new InvalidArgumentException("Malformed where statement! First part of the statement have to be a condition."); |
||||
} |
||||
|
||||
// used to prioritize the "and" operation. |
||||
$orResults = []; |
||||
|
||||
// use results array to get the return value of the conditions within the bracket |
||||
while(!empty($results) || !empty($orResults)){ |
||||
|
||||
if(empty($results)) { |
||||
if($returnValue === true){ |
||||
// we need to check anymore, because the result of true || false is true |
||||
break; |
||||
} |
||||
// $orResults is not empty. |
||||
$nextResult = array_shift($orResults); |
||||
$returnValue = $returnValue || $nextResult; |
||||
continue; |
||||
} |
||||
|
||||
$operationOrNextResult = array_shift($results); |
||||
|
||||
if(is_string($operationOrNextResult)){ |
||||
$operation = $operationOrNextResult; |
||||
|
||||
if(empty($results)){ |
||||
throw new InvalidArgumentException("Malformed where statement! Last part of a condition can not be a operation."); |
||||
} |
||||
$nextResult = array_shift($results); |
||||
|
||||
if(!is_bool($nextResult)){ |
||||
throw new InvalidArgumentException("Malformed where statement! Two operations in a row are not allowed."); |
||||
} |
||||
} else if(is_bool($operationOrNextResult)){ |
||||
$operation = "AND"; |
||||
$nextResult = $operationOrNextResult; |
||||
} else { |
||||
throw new InvalidArgumentException("Malformed where statement! A where statement have to contain just operations and conditions."); |
||||
} |
||||
|
||||
if(!in_array(strtolower($operation), ["and", "or"])){ |
||||
$operation = (!is_object($operation) && !is_array($operation) && !is_null($operation)) ? $operation : gettype($operation); |
||||
throw new InvalidArgumentException("Expected 'and' or 'or' operator got \"$operation\""); |
||||
} |
||||
|
||||
// prepare $orResults execute after all "and" are done. |
||||
if(strtolower($operation) === "or"){ |
||||
$orResults[] = $returnValue; |
||||
$returnValue = $nextResult; |
||||
continue; |
||||
} |
||||
|
||||
$returnValue = $returnValue && $nextResult; |
||||
|
||||
} |
||||
|
||||
return $returnValue; |
||||
} |
||||
|
||||
/** |
||||
* @param array $results |
||||
* @param array $currentDocument |
||||
* @param array $distinctFields |
||||
* @return bool |
||||
*/ |
||||
public static function handleDistinct(array $results, array $currentDocument, array $distinctFields): bool |
||||
{ |
||||
// Distinct data check. |
||||
foreach ($results as $result) { |
||||
foreach ($distinctFields as $field) { |
||||
try { |
||||
$storePassed = (NestedHelper::getNestedValue($field, $result) !== NestedHelper::getNestedValue($field, $currentDocument)); |
||||
} catch (Throwable $th) { |
||||
continue; |
||||
} |
||||
if ($storePassed === false) { |
||||
return false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* @param array $data |
||||
* @param bool $storePassed |
||||
* @param array $nestedWhereConditions |
||||
* @return bool |
||||
* @throws InvalidArgumentException |
||||
* @deprecated since version 2.3, use handleWhereConditions instead. |
||||
*/ |
||||
public static function handleNestedWhere(array $data, bool $storePassed, array $nestedWhereConditions): bool |
||||
{ |
||||
// TODO remove nested where with v3.0 |
||||
|
||||
if(empty($nestedWhereConditions)){ |
||||
return $storePassed; |
||||
} |
||||
|
||||
// the outermost operation specify how the given conditions are connected with other conditions, |
||||
// like the ones that are specified using the where, orWhere, in or notIn methods |
||||
$outerMostOperation = (array_keys($nestedWhereConditions))[0]; |
||||
$nestedConditions = $nestedWhereConditions[$outerMostOperation]; |
||||
|
||||
// specifying outermost is optional and defaults to "and" |
||||
$outerMostOperation = (is_string($outerMostOperation)) ? strtolower($outerMostOperation) : "and"; |
||||
|
||||
// if the document already passed the store with another condition, we dont need to check it. |
||||
if($outerMostOperation === "or" && $storePassed === true){ |
||||
return true; |
||||
} |
||||
|
||||
return self::_nestedWhereHelper($nestedConditions, $data); |
||||
} |
||||
|
||||
/** |
||||
* @param array $element |
||||
* @param array $data |
||||
* @return bool |
||||
* @throws InvalidArgumentException |
||||
* @deprecated since version 2.3. use _handleWhere instead |
||||
*/ |
||||
private static function _nestedWhereHelper(array $element, array $data): bool |
||||
{ |
||||
// TODO remove nested where with v3.0 |
||||
// element is a where condition |
||||
if(array_keys($element) === range(0, (count($element) - 1)) && is_string($element[0])){ |
||||
if(count($element) !== 3){ |
||||
throw new InvalidArgumentException("Where conditions have to be [fieldName, condition, value]"); |
||||
} |
||||
|
||||
$fieldValue = NestedHelper::getNestedValue($element[0], $data); |
||||
|
||||
return self::verifyCondition($element[1], $fieldValue, $element[2]); |
||||
} |
||||
|
||||
// element is an array "brackets" |
||||
|
||||
// prepare results array - example: [true, "and", false] |
||||
$results = []; |
||||
foreach ($element as $value){ |
||||
if(is_array($value)){ |
||||
$results[] = self::_nestedWhereHelper($value, $data); |
||||
} else if (is_string($value)){ |
||||
$results[] = $value; |
||||
} else { |
||||
$value = (!is_object($value) && !is_array($value)) ? $value : gettype($value); |
||||
throw new InvalidArgumentException("Invalid nested where statement element! Expected condition or operation, got: \"$value\""); |
||||
} |
||||
} |
||||
|
||||
if(count($results) < 3){ |
||||
throw new InvalidArgumentException("Malformed nested where statement! A condition consists of at least 3 elements."); |
||||
} |
||||
|
||||
// first result as default value |
||||
$returnValue = array_shift($results); |
||||
|
||||
// use results array to get the return value of the conditions within the bracket |
||||
while(!empty($results)){ |
||||
$operation = array_shift($results); |
||||
$nextResult = array_shift($results); |
||||
|
||||
if(((count($results) % 2) !== 0)){ |
||||
throw new InvalidArgumentException("Malformed nested where statement!"); |
||||
} |
||||
|
||||
if(!is_string($operation) || !in_array(strtolower($operation), ["and", "or"])){ |
||||
$operation = (!is_object($operation) && !is_array($operation)) ? $operation : gettype($operation); |
||||
throw new InvalidArgumentException("Expected 'and' or 'or' operator got \"$operation\""); |
||||
} |
||||
|
||||
if(strtolower($operation) === "and"){ |
||||
$returnValue = $returnValue && $nextResult; |
||||
} else { |
||||
$returnValue = $returnValue || $nextResult; |
||||
} |
||||
} |
||||
|
||||
return $returnValue; |
||||
} |
||||
|
||||
/** |
||||
* @param $value |
||||
* @return int |
||||
* @throws InvalidArgumentException |
||||
*/ |
||||
private static function convertValueToTimeStamp($value): int |
||||
{ |
||||
$value = (is_string($value)) ? trim($value) : $value; |
||||
try{ |
||||
return (new DateTime($value))->getTimestamp(); |
||||
} catch (Exception $exception){ |
||||
$value = (!is_object($value) && !is_array($value)) |
||||
? $value |
||||
: gettype($value); |
||||
throw new InvalidArgumentException( |
||||
"DateTime object given as value to check against. " |
||||
. "Could not convert value into DateTime. " |
||||
. "Value: $value" |
||||
); |
||||
} |
||||
} |
||||
} |
@ -1,387 +0,0 @@
|
||||
<?php |
||||
|
||||
|
||||
namespace SleekDB\Classes; |
||||
|
||||
|
||||
use Exception; |
||||
use SleekDB\Exceptions\InvalidArgumentException; |
||||
use SleekDB\Exceptions\IOException; |
||||
use SleekDB\Query; |
||||
use SleekDB\Store; |
||||
|
||||
/** |
||||
* Class DocumentFinder |
||||
* Find documents |
||||
*/ |
||||
class DocumentFinder |
||||
{ |
||||
protected $storePath; |
||||
protected $queryBuilderProperties; |
||||
protected $primaryKey; |
||||
|
||||
public function __construct(string $storePath, array $queryBuilderProperties, string $primaryKey) |
||||
{ |
||||
$this->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]); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,665 +0,0 @@
|
||||
<?php |
||||
|
||||
|
||||
namespace SleekDB\Classes; |
||||
|
||||
|
||||
use Closure; |
||||
use SleekDB\Exceptions\InvalidArgumentException; |
||||
use SleekDB\Exceptions\IOException; |
||||
use SleekDB\QueryBuilder; |
||||
use SleekDB\SleekDB; |
||||
|
||||
/** |
||||
* Class DocumentReducer |
||||
* Alters one or multiple documents |
||||
*/ |
||||
class DocumentReducer |
||||
{ |
||||
|
||||
const SELECT_FUNCTIONS = [ |
||||
"AVG" => "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); |