This commit is contained in:
2026-07-12 20:10:20 +02:00
parent 2e66b8f936
commit efd1a91b1d
19 changed files with 4723 additions and 10 deletions
+129
View File
@@ -0,0 +1,129 @@
<?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;
}
}
@@ -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"
);
}
}
}
+387
View File
@@ -0,0 +1,387 @@
<?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]);
}
}
}
}
+665
View File
@@ -0,0 +1,665 @@
<?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);
$functionParameters = $fieldToSelect[$function];
$newDocument[$fieldName] = self::handleSelectFunction($function, $document, $functionParameters);
} else if($fieldToSelect instanceof Closure){
$function = self::SELECT_FUNCTIONS['CUSTOM'];
$functionParameters = $fieldToSelect;
$newDocument[$fieldName] = self::handleSelectFunction($function, $document, $functionParameters);
} else {
// No select function is used (fieldToSelect is string or int)
$fieldValue = NestedHelper::getNestedValue((string) $fieldToSelect, $document);
$createdArray = NestedHelper::createNestedArray($fieldName, $fieldValue);
if(!empty($createdArray)){
$createdArrayKey = array_keys($createdArray)[0];
$newDocument[$createdArrayKey] = $createdArray[$createdArrayKey];
}
}
}
$document = $newDocument;
}
}
/**
* @param string $function
* @param array $document
* @param string|array|int|Closure $functionParameters
* @return mixed
* @throws InvalidArgumentException
*/
private static function handleSelectFunction(string $function, array $document, $functionParameters){
if(is_int($functionParameters)){
$functionParameters = (string) $functionParameters;
}
switch (strtolower($function)){
case self::SELECT_FUNCTIONS["ROUND"]:
list($field, $precision) = self::getFieldNamesOfSelectFunction($function, $functionParameters);
if(!is_string($field) || !is_int($precision)){
$errorMsg = "When using the select function \"$function\" the field parameter has to be a string "
."and the precision parameter has to be an integer";
throw new InvalidArgumentException($errorMsg);
}
$data = NestedHelper::getNestedValue($field, $document);
if(!is_numeric($data)){
return null;
}
return round((float) $data, $precision);
case self::SELECT_FUNCTIONS["ABS"]:
list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters);
$data = NestedHelper::getNestedValue($field, $document);
if(!is_numeric($data)){
return null;
}
return abs($data);
case self::SELECT_FUNCTIONS["POSITION"]:
list($field, $subString) = self::getFieldNamesOfSelectFunction($function, $functionParameters);
if(!is_string($subString) || !is_string($field)){
$errorMsg = "When using the select function \"$function\" the subString and field parameters has to be strings";
throw new InvalidArgumentException($errorMsg);
}
$data = NestedHelper::getNestedValue($field, $document);
if(!is_string($data)){
return null;
}
$result = strpos($data, $subString);
return ($result !== false)? $result + 1 : null;
case self::SELECT_FUNCTIONS["UPPER"]:
list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters);
$data = NestedHelper::getNestedValue($field, $document);
if(!is_string($data)){
return null;
}
return strtoupper($data);
case self::SELECT_FUNCTIONS["LOWER"]:
list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters);
$data = NestedHelper::getNestedValue($field, $document);
if(!is_string($data)){
return null;
}
return strtolower($data);
case self::SELECT_FUNCTIONS["LENGTH"]:
list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters);
$data = NestedHelper::getNestedValue($field, $document);
if(is_string($data)){
return strlen($data);
}
if(is_array($data)){
return count($data);
}
return null;
case self::SELECT_FUNCTIONS["CONCAT"]:
list($fields, $glue) = self::getFieldNamesOfSelectFunction($function, $functionParameters);
$result = "";
foreach ($fields as $field){
$data = NestedHelper::getNestedValue($field, $document);
// convertible to string
if(
( !is_array( $data ) )
&& ($data !== "" && $data !== null)
&& (
( !is_object( $data ) && settype( $data, 'string' ) !== false )
|| ( is_object( $data ) && method_exists( $data, '__toString' ) )
)
)
{
if($result !== ""){
$result .= $glue;
}
$result .= $data;
}
}
return ($result !== "") ? $result : null;
case self::SELECT_FUNCTIONS["SUM"]:
list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters);
$data = NestedHelper::getNestedValue($field, $document);
if(!is_array($data)){
return null;
}
$result = 0;
$allEntriesNull = true;
foreach ($data as $value){
if(!is_null($value)){
$result += $value;
$allEntriesNull = false;
}
}
if($allEntriesNull === true){
return null;
}
return $result;
case self::SELECT_FUNCTIONS["MIN"]:
list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters);
$data = NestedHelper::getNestedValue($field, $document);
if(!is_array($data)){
return null;
}
$result = INF;
$allEntriesNull = true;
foreach ($data as $value){
if(!is_null($value)){
if($value < $result){
$result = $value;
}
$allEntriesNull = false;
}
}
if($allEntriesNull === true){
return null;
}
return $result;
case self::SELECT_FUNCTIONS["MAX"]:
list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters);
$data = NestedHelper::getNestedValue($field, $document);
if(!is_array($data)){
return null;
}
$result = -INF;
$allEntriesNull = true;
foreach ($data as $value){
if($value > $result && !is_null($value)) {
$result = $value;
$allEntriesNull = false;
}
}
if($allEntriesNull === true){
return null;
}
return $result;
case self::SELECT_FUNCTIONS["AVG"]:
list($field) = self::getFieldNamesOfSelectFunction($function, $functionParameters);
$data = NestedHelper::getNestedValue($field, $document);
if(!is_array($data)){
return null;
}
$result = 0;
$resultValueAmount = (count($data) + 1);
$allEntriesNull = true;
foreach ($data as $value){
if(!is_null($value)){
$result += $value;
$allEntriesNull = false;
}
}
if($allEntriesNull === true){
return null;
}
return ($result / $resultValueAmount);
case self::SELECT_FUNCTIONS['CUSTOM']:
if(!($functionParameters instanceof Closure)){
throw new InvalidArgumentException("When using a custom select function you need to provide a closure.");
}
return $functionParameters($document);
default:
throw new InvalidArgumentException("The given select function \"$function\" is not supported.");
}
}
/**
* @param string $function
* @param string|array|int $functionParameters
* @return array [array|string $fieldNames, $addition]
* @throws InvalidArgumentException
*/
private static function getFieldNamesOfSelectFunction(string $function, $functionParameters): array
{
if(is_int($functionParameters)){
$functionParameters = (string) $functionParameters;
}
$function = strtolower($function);
switch ($function){
case self::SELECT_FUNCTIONS["ROUND"]:
case self::SELECT_FUNCTIONS["POSITION"]:
if(!is_array($functionParameters) || count($functionParameters) !== 2){
$type = gettype($functionParameters);
$length = (is_array($functionParameters)) ? count($functionParameters) : 0;
$errorMsg = "When using the select function \"$function\" the parameter "
."has to be an array with length = 2, got $type with length $length";
throw new InvalidArgumentException($errorMsg);
}
list($firstParameter, $secondParameter) = $functionParameters;
if($function === self::SELECT_FUNCTIONS["ROUND"]){
$field = $firstParameter;
$addition = $secondParameter;
} else {
$field = $secondParameter;
$addition = $firstParameter;
}
return [$field, $addition];
case self::SELECT_FUNCTIONS["ABS"]:
case self::SELECT_FUNCTIONS["UPPER"]:
case self::SELECT_FUNCTIONS["LOWER"]:
case self::SELECT_FUNCTIONS["LENGTH"]:
case self::SELECT_FUNCTIONS["SUM"]:
case self::SELECT_FUNCTIONS["MIN"]:
case self::SELECT_FUNCTIONS["MAX"]:
case self::SELECT_FUNCTIONS["AVG"]:
if(!is_string($functionParameters)){
$type = gettype($functionParameters);
$errorMsg = "When using the select function \"$function\" the parameter "
."has to be a string, got $type.";
throw new InvalidArgumentException($errorMsg);
}
return [$functionParameters, null];
case self::SELECT_FUNCTIONS["CONCAT"]:
if(!is_array($functionParameters) || count($functionParameters) < 3){
$type = gettype($functionParameters);
$length = (is_array($functionParameters)) ? count($functionParameters) : 0;
$errorMsg = "When using the select function \"$function\" the parameter "
."has to be an array with length > 3, got $type with length $length";
throw new InvalidArgumentException($errorMsg);
}
list($glue) = $functionParameters;
unset($functionParameters[array_keys($functionParameters)[0]]);
return [$functionParameters, $glue];
default:
throw new InvalidArgumentException("The given select function \"$function\" is not supported.");
}
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
namespace SleekDB\Classes;
use SleekDB\Exceptions\InvalidArgumentException;
use SleekDB\Exceptions\IOException;
use SleekDB\Query;
use SleekDB\Store;
/**
* Class DocumentUpdater
* Update and/or delete documents
*/
class DocumentUpdater
{
protected $storePath;
protected $primaryKey;
public function __construct(string $storePath, string $primaryKey)
{
$this->storePath = $storePath;
$this->primaryKey = $primaryKey;
}
/**
* Update one or multiple documents, based on current query
* @param array $results
* @param array $updatable
* @param bool $returnUpdatedDocuments
* @return array|bool
* @throws IOException
*/
public function updateResults(array $results, array $updatable, bool $returnUpdatedDocuments)
{
if(count($results) === 0) {
return false;
}
$primaryKey = $this->primaryKey;
$dataPath = $this->getDataPath();
// check if all documents exist beforehand
foreach ($results as $key => $data) {
$primaryKeyValue = IoHelper::secureStringForFileAccess($data[$primaryKey]);
$data[$primaryKey] = (int) $primaryKeyValue;
$results[$key] = $data;
$filePath = $dataPath . $primaryKeyValue . '.json';
if(!file_exists($filePath)){
return false;
}
}
foreach ($results as $key => $data){
$filePath = $dataPath . $data[$primaryKey] . '.json';
foreach ($updatable as $fieldName => $value) {
// Do not update the primary key reserved index of a store.
if ($fieldName !== $primaryKey) {
NestedHelper::updateNestedValue($fieldName, $data, $value);
}
}
IoHelper::writeContentToFile($filePath, json_encode($data));
$results[$key] = $data;
}
return ($returnUpdatedDocuments === true) ? $results : true;
}
/**
* Deletes matched store objects.
* @param array $results
* @param int $returnOption
* @return bool|array|int
* @throws IOException
* @throws InvalidArgumentException
*/
public function deleteResults(array $results, int $returnOption)
{
$primaryKey = $this->primaryKey;
$dataPath = $this->getDataPath();
switch ($returnOption){
case Query::DELETE_RETURN_BOOL:
$returnValue = !empty($results);
break;
case Query::DELETE_RETURN_COUNT:
$returnValue = count($results);
break;
case Query::DELETE_RETURN_RESULTS:
$returnValue = $results;
break;
default:
throw new InvalidArgumentException("Return option \"$returnOption\" is not supported");
}
if (empty($results)) {
return $returnValue;
}
// TODO implement beforehand check
foreach ($results as $key => $data) {
$primaryKeyValue = IoHelper::secureStringForFileAccess($data[$primaryKey]);
$filePath = $dataPath . $primaryKeyValue . '.json';
if(false === IoHelper::deleteFile($filePath)){
throw new IOException(
'Unable to delete document!
Already deleted documents: '.$key.'.
Location: "' . $filePath .'"'
);
}
}
return $returnValue;
}
/**
* @param array $results
* @param array $fieldsToRemove
* @return array|false
* @throws IOException
*/
public function removeFields(array &$results, array $fieldsToRemove)
{
$primaryKey = $this->primaryKey;
$dataPath = $this->getDataPath();
// check if all documents exist beforehand
foreach ($results as $key => $data) {
$primaryKeyValue = IoHelper::secureStringForFileAccess($data[$primaryKey]);
$data[$primaryKey] = $primaryKeyValue;
$results[$key] = $data;
$filePath = $dataPath . $primaryKeyValue . '.json';
if(!file_exists($filePath)){
return false;
}
}
foreach ($results as &$document){
foreach ($fieldsToRemove as $fieldToRemove){
if($fieldToRemove !== $primaryKey){
NestedHelper::removeNestedField($document, $fieldToRemove);
}
}
$filePath = $dataPath . $document[$primaryKey] . '.json';
IoHelper::writeContentToFile($filePath, json_encode($document));
}
return $results;
}
/**
* @return string
*/
private function getDataPath(): string
{
return $this->storePath . Store::dataDirectory;
}
}
+252
View File
@@ -0,0 +1,252 @@
<?php
namespace SleekDB\Classes;
use Closure;
use Exception;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SleekDB\Exceptions\IOException;
use SleekDB\Exceptions\JsonException;
/**
* Class IoHelper
* Helper to handle file input/ output.
*/
class IoHelper {
/**
* @param string $path
* @throws IOException
*/
public static function checkWrite(string $path)
{
if(file_exists($path) === false){
$path = dirname($path);
}
// Check if PHP has write permission
if (!is_writable($path)) {
throw new IOException(
"Directory or file is not writable at \"$path\". Please change permission."
);
}
}
/**
* @param string $path
* @throws IOException
*/
public static function checkRead(string $path)
{
// Check if PHP has read permission
if (!is_readable($path)) {
throw new IOException(
"Directory or file is not readable at \"$path\". Please change permission."
);
}
}
/**
* @param string $filePath
* @return string
* @throws IOException
*/
public static function getFileContent(string $filePath): string
{
self::checkRead($filePath);
if(!file_exists($filePath)) {
throw new IOException("File does not exist: $filePath");
}
$content = false;
$fp = fopen($filePath, 'rb');
if(flock($fp, LOCK_SH)){
$content = stream_get_contents($fp);
}
flock($fp, LOCK_UN);
fclose($fp);
if($content === false) {
throw new IOException("Could not retrieve the content of a file. Please check permissions at: $filePath");
}
return $content;
}
/**
* @param string $filePath
* @param string $content
* @throws IOException
*/
public static function writeContentToFile(string $filePath, string $content){
self::checkWrite($filePath);
// Wait until it's unlocked, then write.
if(file_put_contents($filePath, $content, LOCK_EX) === false){
throw new IOException("Could not write content to file. Please check permissions at: $filePath");
}
}
/**
* @param string $folderPath
* @return bool
* @throws IOException
*/
public static function deleteFolder(string $folderPath): bool
{
self::checkWrite($folderPath);
$it = new RecursiveDirectoryIterator($folderPath, RecursiveDirectoryIterator::SKIP_DOTS);
$files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file) {
self::checkWrite($file);
if ($file->isDir()) {
rmdir($file->getRealPath());
} else {
unlink($file->getRealPath());
}
}
return rmdir($folderPath);
}
/**
* @param string $folderPath
* @param int $chmod
* @throws IOException
*/
public static function createFolder(string $folderPath, int $chmod){
// We don't need to create a folder if it already exists.
if(file_exists($folderPath) === true){
return;
}
self::checkWrite($folderPath);
// Check if the data_directory exists or create one.
if (!file_exists($folderPath) && !mkdir($folderPath, $chmod, true) && !is_dir($folderPath)) {
throw new IOException(
'Unable to create the a directory at ' . $folderPath
);
}
}
/**
* @param string $filePath
* @param Closure $updateContentFunction Has to return a string or an array that will be encoded to json.
* @return string
* @throws IOException
* @throws JsonException
*/
public static function updateFileContent(string $filePath, Closure $updateContentFunction): string
{
self::checkRead($filePath);
self::checkWrite($filePath);
$content = false;
$fp = fopen($filePath, 'rb');
if(flock($fp, LOCK_SH)){
$content = stream_get_contents($fp);
}
flock($fp, LOCK_UN);
fclose($fp);
if($content === false){
throw new IOException("Could not get shared lock for file: $filePath");
}
$content = $updateContentFunction($content);
if(!is_string($content)){
$encodedContent = json_encode($content);
if($encodedContent === false){
$content = (!is_object($content) && !is_array($content) && !is_null($content)) ? $content : gettype($content);
throw new JsonException("Could not encode content with json_encode. Content: \"$content\".");
}
$content = $encodedContent;
}
if(file_put_contents($filePath, $content, LOCK_EX) === false){
throw new IOException("Could not write content to file. Please check permissions at: $filePath");
}
return $content;
}
/**
* @param string $filePath
* @return bool
*/
public static function deleteFile(string $filePath): bool
{
if(false === file_exists($filePath)){
return true;
}
try{
self::checkWrite($filePath);
}catch(Exception $exception){
return false;
}
return (@unlink($filePath) && !file_exists($filePath));
}
/**
* @param array $filePaths
* @return bool
*/
public static function deleteFiles(array $filePaths): bool
{
foreach ($filePaths as $filePath){
// if a file does not exist, we do not need to delete it.
if(true === file_exists($filePath)){
try{
self::checkWrite($filePath);
if(false === @unlink($filePath) || file_exists($filePath)){
return false;
}
} catch (Exception $exception){
// TODO trigger a warning or exception
return false;
}
}
}
return true;
}
/**
* Strip string for secure file access.
* @param string $string
* @return string
*/
public static function secureStringForFileAccess(string $string): string
{
return (str_replace(array(".", "/", "\\"), "", $string));
}
/**
* Appends a slash ("/") to the given directory path if there is none.
* @param string $directory
*/
public static function normalizeDirectory(string &$directory){
if(!empty($directory) && substr($directory, -1) !== "/") {
$directory .= "/";
}
}
/**
* Returns the amount of files in folder.
* @param string $folder
* @return int
* @throws IOException
*/
public static function countFolderContent(string $folder): int
{
self::checkRead($folder);
$fi = new \FilesystemIterator($folder, \FilesystemIterator::SKIP_DOTS);
return iterator_count($fi);
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
namespace SleekDB\Classes;
use SleekDB\Exceptions\InvalidArgumentException;
/**
* Class NestedHelper
* Helper to handle arrays.
*/
class NestedHelper
{
/**
* Get nested properties of a store object.
* @param string $fieldName
* @param array $data
* @return mixed
* @throws InvalidArgumentException
*/
public static function getNestedValue(string $fieldName, array $data)
{
$fieldName = trim($fieldName);
if (empty($fieldName)) {
throw new InvalidArgumentException('fieldName is not allowed to be empty');
}
// Dive deep step by step.
foreach (explode('.', $fieldName) as $i) {
// If the field does not exists we return null;
if (!isset($data[$i])) {
return null;
}
// The index is valid, collect the data.
$data = $data[$i];
}
return $data;
}
/**
* Check if a nested Property exists
* @param string $fieldName
* @param array $data
* @return mixed
* @throws InvalidArgumentException
*/
public static function nestedFieldExists(string $fieldName, array $data)
{
$fieldName = trim($fieldName);
if (empty($fieldName)) {
throw new InvalidArgumentException('fieldName is not allowed to be empty');
}
// Dive deep step by step.
foreach (explode('.', $fieldName) as $i) {
// check if field exists
if (!is_array($data) || !array_key_exists($i, $data)) {
return false;
}
// The index is valid, dive deeper.
$data = $data[$i];
}
return true;
}
public static function updateNestedValue(string $fieldName, array &$data, $newValue){
$fieldNameArray = explode(".", $fieldName);
$value = $newValue;
if(count($fieldNameArray) > 1){
$data = self::_updateNestedValueHelper($fieldNameArray, $data, $newValue, count($fieldNameArray));
return;
}
$data[$fieldNameArray[0]] = $value;
}
public static function createNestedArray(string $fieldName, $fieldValue): array
{
$temp = [];
$fieldNameArray = explode('.', $fieldName);
$fieldNameArrayReverse = array_reverse($fieldNameArray);
foreach ($fieldNameArrayReverse as $index => $i) {
if($index === 0){
$temp = array($i => $fieldValue);
} else {
$temp = array($i => $temp);
}
}
return $temp;
}
public static function removeNestedField(array &$document, string $fieldToRemove){
if (array_key_exists($fieldToRemove, $document)) {
unset($document[$fieldToRemove]);
return;
}
// should be a nested array at this point
$temp = &$document;
$fieldNameArray = explode('.', $fieldToRemove);
$fieldNameArrayCount = count($fieldNameArray);
foreach ($fieldNameArray as $index => $i) {
// last iteration
if(($fieldNameArrayCount - 1) === $index){
if(is_array($temp) && array_key_exists($i, $temp)) {
unset($temp[$i]);
}
break;
}
if(!is_array($temp) || !array_key_exists($i, $temp)){
break;
}
$temp = &$temp[$i];
}
}
/**
* @param array $keysArray
* @param $data
* @param $newValue
* @param int $originalKeySize
* @return mixed
*/
private static function _updateNestedValueHelper(array $keysArray, $data, $newValue, int $originalKeySize)
{
if(empty($keysArray)){
return $newValue;
}
$currentKey = $keysArray[0];
$result = (is_array($data)) ? $data : [];
if(!is_array($data) || !array_key_exists($currentKey, $data)){
$result[$currentKey] = self::_updateNestedValueHelper(array_slice($keysArray, 1), $data, $newValue, $originalKeySize);
if(count($keysArray) !== $originalKeySize){
return $result;
}
}
$result[$currentKey] = self::_updateNestedValueHelper(array_slice($keysArray, 1), $data[$currentKey], $newValue, $originalKeySize);
return $result;
}
}