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; } }