update
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
<?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 = 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user