517 lines
13 KiB
PHP
517 lines
13 KiB
PHP
<?php
|
|
|
|
namespace SleekDB;
|
|
|
|
use Closure;
|
|
use SleekDB\Exceptions\InvalidArgumentException;
|
|
|
|
class QueryBuilder
|
|
{
|
|
|
|
/**
|
|
* @var Store
|
|
*/
|
|
protected $store;
|
|
/**
|
|
* @var Cache
|
|
*/
|
|
protected $cache;
|
|
|
|
protected $whereConditions = [];
|
|
|
|
protected $skip = 0;
|
|
protected $limit = 0;
|
|
protected $orderBy = [];
|
|
protected $nestedWhere = []; // TODO remove with version 3.0
|
|
protected $search = [];
|
|
protected $searchOptions = [
|
|
"minLength" => 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;
|
|
}
|
|
|
|
}
|