classe sleekDB
This commit is contained in:
parent
a16550b0cd
commit
b64c92bf22
309
core/class/sleekDB/Cache.php
Normal file
309
core/class/sleekDB/Cache.php
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SleekDB;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Exception;
|
||||||
|
use ReflectionFunction;
|
||||||
|
use SleekDB\Classes\IoHelper;
|
||||||
|
use SleekDB\Exceptions\IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Cache
|
||||||
|
* Caching layer of SleekDB, handles everything regarding caching.
|
||||||
|
*/
|
||||||
|
class Cache
|
||||||
|
{
|
||||||
|
|
||||||
|
const DEFAULT_CACHE_DIR = "cache/";
|
||||||
|
const NO_LIFETIME_FILE_STRING = "no_lifetime";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifetime in seconds or deletion with deleteAll
|
||||||
|
* @var int|null
|
||||||
|
*/
|
||||||
|
protected $lifetime;
|
||||||
|
|
||||||
|
protected $cachePath = "";
|
||||||
|
|
||||||
|
protected $cacheDir = "";
|
||||||
|
|
||||||
|
protected $tokenArray;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache constructor.
|
||||||
|
* @param string $storePath
|
||||||
|
* @param array $cacheTokenArray
|
||||||
|
* @param int|null $cacheLifetime
|
||||||
|
*/
|
||||||
|
public function __construct(string $storePath, array &$cacheTokenArray, $cacheLifetime)
|
||||||
|
{
|
||||||
|
// TODO make it possible to define custom cache directory.
|
||||||
|
// $cacheDir = "";
|
||||||
|
// $this->setCacheDir($cacheDir);
|
||||||
|
|
||||||
|
$this->setCachePath($storePath);
|
||||||
|
|
||||||
|
$this->setTokenArray($cacheTokenArray);
|
||||||
|
|
||||||
|
$this->lifetime = $cacheLifetime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the cache lifetime for current query.
|
||||||
|
* @return int|null lifetime in seconds (int) or no lifetime with null
|
||||||
|
*/
|
||||||
|
public function getLifetime()
|
||||||
|
{
|
||||||
|
return $this->lifetime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the path to cache folder of current store.
|
||||||
|
* @return string path to cache directory
|
||||||
|
*/
|
||||||
|
public function getCachePath(): string
|
||||||
|
{
|
||||||
|
return $this->cachePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the cache token used as filename to store cache file.
|
||||||
|
* @return string unique token for current query.
|
||||||
|
*/
|
||||||
|
public function getToken(): string
|
||||||
|
{
|
||||||
|
$tokenArray = $this->getTokenArray();
|
||||||
|
$tokenArray = self::convertClosuresToString($tokenArray);
|
||||||
|
|
||||||
|
return md5(json_encode($tokenArray));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all cache files for current store.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function deleteAll(): bool
|
||||||
|
{
|
||||||
|
return IoHelper::deleteFiles(glob($this->getCachePath()."*"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all cache files with no lifetime (null) in current store.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function deleteAllWithNoLifetime(): bool
|
||||||
|
{
|
||||||
|
$noLifetimeFileString = self::NO_LIFETIME_FILE_STRING;
|
||||||
|
return IoHelper::deleteFiles(glob($this->getCachePath()."*.$noLifetimeFileString.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save content for current query as a cache file.
|
||||||
|
* @param array $content
|
||||||
|
* @throws IOException if cache folder is not writable or saving failed.
|
||||||
|
*/
|
||||||
|
public function set(array $content){
|
||||||
|
$lifetime = $this->getLifetime();
|
||||||
|
$cachePath = $this->getCachePath();
|
||||||
|
$token = $this->getToken();
|
||||||
|
|
||||||
|
$noLifetimeFileString = self::NO_LIFETIME_FILE_STRING;
|
||||||
|
$cacheFile = $cachePath . $token . ".$noLifetimeFileString.json";
|
||||||
|
|
||||||
|
if(is_int($lifetime)){
|
||||||
|
$cacheFile = $cachePath . $token . ".$lifetime.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
IoHelper::writeContentToFile($cacheFile, json_encode($content));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve content of cache file.
|
||||||
|
* @return array|null array on success, else null
|
||||||
|
* @throws IOException if cache file is not readable or does not exist.
|
||||||
|
*/
|
||||||
|
public function get(){
|
||||||
|
$cachePath = $this->getCachePath();
|
||||||
|
$token = $this->getToken();
|
||||||
|
|
||||||
|
$cacheFile = null;
|
||||||
|
|
||||||
|
IoHelper::checkRead($cachePath);
|
||||||
|
|
||||||
|
$cacheFiles = glob($cachePath.$token."*.json");
|
||||||
|
|
||||||
|
if($cacheFiles !== false && count($cacheFiles) > 0){
|
||||||
|
$cacheFile = $cacheFiles[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!empty($cacheFile)){
|
||||||
|
$cacheParts = explode(".", $cacheFile);
|
||||||
|
if(count($cacheParts) >= 3){
|
||||||
|
$lifetime = $cacheParts[count($cacheParts) - 2];
|
||||||
|
if(is_numeric($lifetime)){
|
||||||
|
if($lifetime === "0"){
|
||||||
|
return json_decode(IoHelper::getFileContent($cacheFile), true);
|
||||||
|
}
|
||||||
|
$fileExpiredAfter = filemtime($cacheFile) + (int) $lifetime;
|
||||||
|
if(time() <= $fileExpiredAfter){
|
||||||
|
return json_decode(IoHelper::getFileContent($cacheFile), true);
|
||||||
|
}
|
||||||
|
IoHelper::deleteFile($cacheFile);
|
||||||
|
} else if($lifetime === self::NO_LIFETIME_FILE_STRING){
|
||||||
|
return json_decode(IoHelper::getFileContent($cacheFile), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete cache file/s for current query.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function delete(): bool
|
||||||
|
{
|
||||||
|
return IoHelper::deleteFiles(glob($this->getCachePath().$this->getToken()."*.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $storePath
|
||||||
|
* @return Cache
|
||||||
|
*/
|
||||||
|
private function setCachePath(string $storePath): Cache
|
||||||
|
{
|
||||||
|
$cachePath = "";
|
||||||
|
$cacheDir = $this->getCacheDir();
|
||||||
|
|
||||||
|
if(!empty($storePath)){
|
||||||
|
IoHelper::normalizeDirectory($storePath);
|
||||||
|
$cachePath = $storePath . $cacheDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cachePath = $cachePath;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the cache token array used for cache token string generation.
|
||||||
|
* @param array $tokenArray
|
||||||
|
* @return Cache
|
||||||
|
*/
|
||||||
|
private function setTokenArray(array &$tokenArray): Cache
|
||||||
|
{
|
||||||
|
$this->tokenArray = &$tokenArray;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the cache token array.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function getTokenArray(): array
|
||||||
|
{
|
||||||
|
return $this->tokenArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert one or multiple closures to string. If array provided, recursively.
|
||||||
|
* @param mixed $data
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
private static function convertClosuresToString($data){
|
||||||
|
if(!is_array($data)){
|
||||||
|
if($data instanceof \Closure){
|
||||||
|
return self::getClosureAsString($data);
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
foreach ($data as $key => $token){
|
||||||
|
if(is_array($token)){
|
||||||
|
$data[$key] = self::convertClosuresToString($token);
|
||||||
|
} else if($token instanceof \Closure){
|
||||||
|
$data[$key] = self::getClosureAsString($token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a string representation of a closure that can be used to differentiate between closures
|
||||||
|
* when generating the cache token string.
|
||||||
|
* @param Closure $closure
|
||||||
|
* @return false|string string representation of closure or false on failure.
|
||||||
|
*/
|
||||||
|
private static function getClosureAsString(Closure $closure)
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
$reflectionFunction = new ReflectionFunction($closure); // get reflection object
|
||||||
|
} catch (Exception $exception){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$filePath = $reflectionFunction->getFileName(); // absolute path of php file containing function
|
||||||
|
$startLine = $reflectionFunction->getStartLine(); // start line of function
|
||||||
|
$endLine = $reflectionFunction->getEndLine(); // end line of function
|
||||||
|
$lineSeparator = PHP_EOL; // line separator "\n"
|
||||||
|
|
||||||
|
$staticVariables = $reflectionFunction->getStaticVariables();
|
||||||
|
$staticVariables = var_export($staticVariables, true);
|
||||||
|
|
||||||
|
if($filePath === false || $startLine === false || $endLine === false){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$startEndDifference = $endLine - $startLine;
|
||||||
|
|
||||||
|
$startLine--; // -1 to use it with the array representation of the file
|
||||||
|
|
||||||
|
if($startLine < 0 || $startEndDifference < 0){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get content of file containing function
|
||||||
|
$fp = fopen($filePath, 'rb');
|
||||||
|
$fileContent = "";
|
||||||
|
if(flock($fp, LOCK_SH)){
|
||||||
|
$fileContent = @stream_get_contents($fp);
|
||||||
|
}
|
||||||
|
flock($fp, LOCK_UN);
|
||||||
|
fclose($fp);
|
||||||
|
|
||||||
|
if(empty($fileContent)){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// separate the file into an array containing every line as one element
|
||||||
|
$fileContentArray = explode($lineSeparator, $fileContent);
|
||||||
|
if(count($fileContentArray) < $endLine){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the part of the file containing the function as a string.
|
||||||
|
$functionString = implode("", array_slice($fileContentArray, $startLine, $startEndDifference + 1));
|
||||||
|
$functionString .= "|staticScopeVariables:".$staticVariables;
|
||||||
|
return $functionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the cache directory name.
|
||||||
|
* @param string $cacheDir
|
||||||
|
* @return Cache
|
||||||
|
*/
|
||||||
|
private function setCacheDir(string $cacheDir): Cache
|
||||||
|
{
|
||||||
|
IoHelper::normalizeDirectory($cacheDir);
|
||||||
|
$this->cacheDir = $cacheDir;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the cache directory name or the default cache directory name if empty.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getCacheDir(): string
|
||||||
|
{
|
||||||
|
return (!empty($this->cacheDir)) ? $this->cacheDir : self::DEFAULT_CACHE_DIR;
|
||||||
|
}
|
||||||
|
}
|
129
core/class/sleekDB/Classes/CacheHandler.php
Normal file
129
core/class/sleekDB/Classes/CacheHandler.php
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
433
core/class/sleekDB/Classes/ConditionsHandler.php
Normal file
433
core/class/sleekDB/Classes/ConditionsHandler.php
Normal file
@ -0,0 +1,433 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace SleekDB\Classes;
|
||||||
|
|
||||||
|
|
||||||
|
use SleekDB\Exceptions\InvalidArgumentException;
|
||||||
|
use DateTime;
|
||||||
|
use Exception;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ConditionsHandler
|
||||||
|
* Handle all types of conditions to check if a document has passed.
|
||||||
|
*/
|
||||||
|
class ConditionsHandler
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the result of an condition.
|
||||||
|
* @param string $condition
|
||||||
|
* @param mixed $fieldValue value of current field
|
||||||
|
* @param mixed $value value to check
|
||||||
|
* @return bool
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public static function verifyCondition(string $condition, $fieldValue, $value): bool
|
||||||
|
{
|
||||||
|
|
||||||
|
if($value instanceof DateTime){
|
||||||
|
// compare timestamps
|
||||||
|
|
||||||
|
// null, false or an empty string will convert to current date and time.
|
||||||
|
// That is not what we want.
|
||||||
|
if(empty($fieldValue)){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$value = $value->getTimestamp();
|
||||||
|
$fieldValue = self::convertValueToTimeStamp($fieldValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
$condition = strtolower(trim($condition));
|
||||||
|
switch ($condition){
|
||||||
|
case "=":
|
||||||
|
case "===":
|
||||||
|
return ($fieldValue === $value);
|
||||||
|
case "==":
|
||||||
|
return ($fieldValue == $value);
|
||||||
|
case "<>":
|
||||||
|
return ($fieldValue != $value);
|
||||||
|
case "!==":
|
||||||
|
case "!=":
|
||||||
|
return ($fieldValue !== $value);
|
||||||
|
case ">":
|
||||||
|
return ($fieldValue > $value);
|
||||||
|
case ">=":
|
||||||
|
return ($fieldValue >= $value);
|
||||||
|
case "<":
|
||||||
|
return ($fieldValue < $value);
|
||||||
|
case "<=":
|
||||||
|
return ($fieldValue <= $value);
|
||||||
|
case "not like":
|
||||||
|
case "like":
|
||||||
|
|
||||||
|
if(!is_string($value)){
|
||||||
|
throw new InvalidArgumentException("When using \"LIKE\" or \"NOT LIKE\" the value has to be a string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// escape characters that are part of regular expression syntax
|
||||||
|
// https://www.php.net/manual/en/function.preg-quote.php
|
||||||
|
// We can not use preg_quote because the following characters are also wildcard characters in sql
|
||||||
|
// so we will not escape them: [ ^ ] -
|
||||||
|
$charactersToEscape = [".", "\\", "+", "*", "?", "$", "(", ")", "{", "}", "=", "!", "<", ">", "|", ":", "#"];
|
||||||
|
foreach ($charactersToEscape as $characterToEscape){
|
||||||
|
$value = str_replace($characterToEscape, "\\".$characterToEscape, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = str_replace(array('%', '_'), array('.*', '.{1}'), $value); // (zero or more characters) and (single character)
|
||||||
|
$pattern = "/^" . $value . "$/i";
|
||||||
|
$result = (preg_match($pattern, $fieldValue) === 1);
|
||||||
|
return ($condition === "not like") ? !$result : $result;
|
||||||
|
|
||||||
|
case "not in":
|
||||||
|
case "in":
|
||||||
|
if(!is_array($value)){
|
||||||
|
$value = (!is_object($value) && !is_array($value) && !is_null($value)) ? $value : gettype($value);
|
||||||
|
throw new InvalidArgumentException("When using \"in\" and \"not in\" you have to check against an array. Got: $value");
|
||||||
|
}
|
||||||
|
if(!empty($value)){
|
||||||
|
(list($firstElement) = $value);
|
||||||
|
if($firstElement instanceof DateTime){
|
||||||
|
// if the user wants to use DateTime, every element of the array has to be an DateTime object.
|
||||||
|
|
||||||
|
// compare timestamps
|
||||||
|
|
||||||
|
// null, false or an empty string will convert to current date and time.
|
||||||
|
// That is not what we want.
|
||||||
|
if(empty($fieldValue)){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($value as $key => $item){
|
||||||
|
if(!($item instanceof DateTime)){
|
||||||
|
throw new InvalidArgumentException("If one DateTime object is given in an \"IN\" or \"NOT IN\" comparison, every element has to be a DateTime object!");
|
||||||
|
}
|
||||||
|
$value[$key] = $item->getTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldValue = self::convertValueToTimeStamp($fieldValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$result = in_array($fieldValue, $value, true);
|
||||||
|
return ($condition === "not in") ? !$result : $result;
|
||||||
|
case "not between":
|
||||||
|
case "between":
|
||||||
|
|
||||||
|
if(!is_array($value) || ($valueLength = count($value)) !== 2){
|
||||||
|
$value = (!is_object($value) && !is_array($value) && !is_null($value)) ? $value : gettype($value);
|
||||||
|
if(isset($valueLength)){
|
||||||
|
$value .= " | Length: $valueLength";
|
||||||
|
}
|
||||||
|
throw new InvalidArgumentException("When using \"between\" you have to check against an array with a length of 2. Got: $value");
|
||||||
|
}
|
||||||
|
|
||||||
|
list($startValue, $endValue) = $value;
|
||||||
|
|
||||||
|
$result = (
|
||||||
|
self::verifyCondition(">=", $fieldValue, $startValue)
|
||||||
|
&& self::verifyCondition("<=", $fieldValue, $endValue)
|
||||||
|
);
|
||||||
|
|
||||||
|
return ($condition === "not between") ? !$result : $result;
|
||||||
|
case "not contains":
|
||||||
|
case "contains":
|
||||||
|
|
||||||
|
if(!is_array($fieldValue)){
|
||||||
|
return ($condition === "not contains");
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldValues = [];
|
||||||
|
|
||||||
|
if($value instanceof DateTime){
|
||||||
|
// compare timestamps
|
||||||
|
$value = $value->getTimestamp();
|
||||||
|
|
||||||
|
foreach ($fieldValue as $item){
|
||||||
|
// null, false or an empty string will convert to current date and time.
|
||||||
|
// That is not what we want.
|
||||||
|
if(empty($item)){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try{
|
||||||
|
$fieldValues[] = self::convertValueToTimeStamp($item);
|
||||||
|
} catch (Exception $exception){
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!empty($fieldValues)){
|
||||||
|
$result = in_array($value, $fieldValues, true);
|
||||||
|
} else {
|
||||||
|
$result = in_array($value, $fieldValue, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($condition === "not contains") ? !$result : $result;
|
||||||
|
case 'exists':
|
||||||
|
return $fieldValue === $value;
|
||||||
|
default:
|
||||||
|
throw new InvalidArgumentException("Condition \"$condition\" is not allowed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $element condition or operation
|
||||||
|
* @param array $data
|
||||||
|
* @return bool
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public static function handleWhereConditions(array $element, array $data): bool
|
||||||
|
{
|
||||||
|
if(empty($element)){
|
||||||
|
throw new InvalidArgumentException("Malformed where statement! Where statements can not contain empty arrays.");
|
||||||
|
}
|
||||||
|
if(array_keys($element) !== range(0, (count($element) - 1))){
|
||||||
|
throw new InvalidArgumentException("Malformed where statement! Associative arrays are not allowed.");
|
||||||
|
}
|
||||||
|
// element is a where condition
|
||||||
|
if(is_string($element[0]) && is_string($element[1])){
|
||||||
|
if(count($element) !== 3){
|
||||||
|
throw new InvalidArgumentException("Where conditions have to be [fieldName, condition, value]");
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldName = $element[0];
|
||||||
|
$condition = strtolower(trim($element[1]));
|
||||||
|
$fieldValue = ($condition === 'exists')
|
||||||
|
? NestedHelper::nestedFieldExists($fieldName, $data)
|
||||||
|
: NestedHelper::getNestedValue($fieldName, $data);
|
||||||
|
|
||||||
|
return self::verifyCondition($condition, $fieldValue, $element[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// element is an array "brackets"
|
||||||
|
|
||||||
|
// prepare results array - example: [true, "and", false]
|
||||||
|
$results = [];
|
||||||
|
foreach ($element as $value){
|
||||||
|
if(is_array($value)){
|
||||||
|
$results[] = self::handleWhereConditions($value, $data);
|
||||||
|
} else if (is_string($value)) {
|
||||||
|
$results[] = $value;
|
||||||
|
} else if($value instanceof \Closure){
|
||||||
|
$result = $value($data);
|
||||||
|
if(!is_bool($result)){
|
||||||
|
$resultType = gettype($result);
|
||||||
|
$errorMsg = "The closure in the where condition needs to return a boolean. Got: $resultType";
|
||||||
|
throw new InvalidArgumentException($errorMsg);
|
||||||
|
}
|
||||||
|
$results[] = $result;
|
||||||
|
} else {
|
||||||
|
$value = (!is_object($value) && !is_array($value) && !is_null($value)) ? $value : gettype($value);
|
||||||
|
throw new InvalidArgumentException("Invalid nested where statement element! Expected condition or operation, got: \"$value\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// first result as default value
|
||||||
|
$returnValue = array_shift($results);
|
||||||
|
|
||||||
|
if(is_bool($returnValue) === false){
|
||||||
|
throw new InvalidArgumentException("Malformed where statement! First part of the statement have to be a condition.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// used to prioritize the "and" operation.
|
||||||
|
$orResults = [];
|
||||||
|
|
||||||
|
// use results array to get the return value of the conditions within the bracket
|
||||||
|
while(!empty($results) || !empty($orResults)){
|
||||||
|
|
||||||
|
if(empty($results)) {
|
||||||
|
if($returnValue === true){
|
||||||
|
// we need to check anymore, because the result of true || false is true
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// $orResults is not empty.
|
||||||
|
$nextResult = array_shift($orResults);
|
||||||
|
$returnValue = $returnValue || $nextResult;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$operationOrNextResult = array_shift($results);
|
||||||
|
|
||||||
|
if(is_string($operationOrNextResult)){
|
||||||
|
$operation = $operationOrNextResult;
|
||||||
|
|
||||||
|
if(empty($results)){
|
||||||
|
throw new InvalidArgumentException("Malformed where statement! Last part of a condition can not be a operation.");
|
||||||
|
}
|
||||||
|
$nextResult = array_shift($results);
|
||||||
|
|
||||||
|
if(!is_bool($nextResult)){
|
||||||
|
throw new InvalidArgumentException("Malformed where statement! Two operations in a row are not allowed.");
|
||||||
|
}
|
||||||
|
} else if(is_bool($operationOrNextResult)){
|
||||||
|
$operation = "AND";
|
||||||
|
$nextResult = $operationOrNextResult;
|
||||||
|
} else {
|
||||||
|
throw new InvalidArgumentException("Malformed where statement! A where statement have to contain just operations and conditions.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!in_array(strtolower($operation), ["and", "or"])){
|
||||||
|
$operation = (!is_object($operation) && !is_array($operation) && !is_null($operation)) ? $operation : gettype($operation);
|
||||||
|
throw new InvalidArgumentException("Expected 'and' or 'or' operator got \"$operation\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare $orResults execute after all "and" are done.
|
||||||
|
if(strtolower($operation) === "or"){
|
||||||
|
$orResults[] = $returnValue;
|
||||||
|
$returnValue = $nextResult;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$returnValue = $returnValue && $nextResult;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return $returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $results
|
||||||
|
* @param array $currentDocument
|
||||||
|
* @param array $distinctFields
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function handleDistinct(array $results, array $currentDocument, array $distinctFields): bool
|
||||||
|
{
|
||||||
|
// Distinct data check.
|
||||||
|
foreach ($results as $result) {
|
||||||
|
foreach ($distinctFields as $field) {
|
||||||
|
try {
|
||||||
|
$storePassed = (NestedHelper::getNestedValue($field, $result) !== NestedHelper::getNestedValue($field, $currentDocument));
|
||||||
|
} catch (Throwable $th) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($storePassed === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $data
|
||||||
|
* @param bool $storePassed
|
||||||
|
* @param array $nestedWhereConditions
|
||||||
|
* @return bool
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @deprecated since version 2.3, use handleWhereConditions instead.
|
||||||
|
*/
|
||||||
|
public static function handleNestedWhere(array $data, bool $storePassed, array $nestedWhereConditions): bool
|
||||||
|
{
|
||||||
|
// TODO remove nested where with v3.0
|
||||||
|
|
||||||
|
if(empty($nestedWhereConditions)){
|
||||||
|
return $storePassed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the outermost operation specify how the given conditions are connected with other conditions,
|
||||||
|
// like the ones that are specified using the where, orWhere, in or notIn methods
|
||||||
|
$outerMostOperation = (array_keys($nestedWhereConditions))[0];
|
||||||
|
$nestedConditions = $nestedWhereConditions[$outerMostOperation];
|
||||||
|
|
||||||
|
// specifying outermost is optional and defaults to "and"
|
||||||
|
$outerMostOperation = (is_string($outerMostOperation)) ? strtolower($outerMostOperation) : "and";
|
||||||
|
|
||||||
|
// if the document already passed the store with another condition, we dont need to check it.
|
||||||
|
if($outerMostOperation === "or" && $storePassed === true){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::_nestedWhereHelper($nestedConditions, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $element
|
||||||
|
* @param array $data
|
||||||
|
* @return bool
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @deprecated since version 2.3. use _handleWhere instead
|
||||||
|
*/
|
||||||
|
private static function _nestedWhereHelper(array $element, array $data): bool
|
||||||
|
{
|
||||||
|
// TODO remove nested where with v3.0
|
||||||
|
// element is a where condition
|
||||||
|
if(array_keys($element) === range(0, (count($element) - 1)) && is_string($element[0])){
|
||||||
|
if(count($element) !== 3){
|
||||||
|
throw new InvalidArgumentException("Where conditions have to be [fieldName, condition, value]");
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldValue = NestedHelper::getNestedValue($element[0], $data);
|
||||||
|
|
||||||
|
return self::verifyCondition($element[1], $fieldValue, $element[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// element is an array "brackets"
|
||||||
|
|
||||||
|
// prepare results array - example: [true, "and", false]
|
||||||
|
$results = [];
|
||||||
|
foreach ($element as $value){
|
||||||
|
if(is_array($value)){
|
||||||
|
$results[] = self::_nestedWhereHelper($value, $data);
|
||||||
|
} else if (is_string($value)){
|
||||||
|
$results[] = $value;
|
||||||
|
} else {
|
||||||
|
$value = (!is_object($value) && !is_array($value)) ? $value : gettype($value);
|
||||||
|
throw new InvalidArgumentException("Invalid nested where statement element! Expected condition or operation, got: \"$value\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(count($results) < 3){
|
||||||
|
throw new InvalidArgumentException("Malformed nested where statement! A condition consists of at least 3 elements.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// first result as default value
|
||||||
|
$returnValue = array_shift($results);
|
||||||
|
|
||||||
|
// use results array to get the return value of the conditions within the bracket
|
||||||
|
while(!empty($results)){
|
||||||
|
$operation = array_shift($results);
|
||||||
|
$nextResult = array_shift($results);
|
||||||
|
|
||||||
|
if(((count($results) % 2) !== 0)){
|
||||||
|
throw new InvalidArgumentException("Malformed nested where statement!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!is_string($operation) || !in_array(strtolower($operation), ["and", "or"])){
|
||||||
|
$operation = (!is_object($operation) && !is_array($operation)) ? $operation : gettype($operation);
|
||||||
|
throw new InvalidArgumentException("Expected 'and' or 'or' operator got \"$operation\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(strtolower($operation) === "and"){
|
||||||
|
$returnValue = $returnValue && $nextResult;
|
||||||
|
} else {
|
||||||
|
$returnValue = $returnValue || $nextResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $value
|
||||||
|
* @return int
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
private static function convertValueToTimeStamp($value): int
|
||||||
|
{
|
||||||
|
$value = (is_string($value)) ? trim($value) : $value;
|
||||||
|
try{
|
||||||
|
return (new DateTime($value))->getTimestamp();
|
||||||
|
} catch (Exception $exception){
|
||||||
|
$value = (!is_object($value) && !is_array($value))
|
||||||
|
? $value
|
||||||
|
: gettype($value);
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"DateTime object given as value to check against. "
|
||||||
|
. "Could not convert value into DateTime. "
|
||||||
|
. "Value: $value"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
387
core/class/sleekDB/Classes/DocumentFinder.php
Normal file
387
core/class/sleekDB/Classes/DocumentFinder.php
Normal 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
core/class/sleekDB/Classes/DocumentReducer.php
Normal file
665
core/class/sleekDB/Classes/DocumentReducer.php
Normal 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
core/class/sleekDB/Classes/DocumentUpdater.php
Normal file
159
core/class/sleekDB/Classes/DocumentUpdater.php
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
251
core/class/sleekDB/Classes/IoHelper.php
Normal file
251
core/class/sleekDB/Classes/IoHelper.php
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
<?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
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public static function createFolder(string $folderPath){
|
||||||
|
// 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, 0777, 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
core/class/sleekDB/Classes/NestedHelper.php
Normal file
140
core/class/sleekDB/Classes/NestedHelper.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
7
core/class/sleekDB/Exceptions/IOException.php
Normal file
7
core/class/sleekDB/Exceptions/IOException.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SleekDB\Exceptions;
|
||||||
|
|
||||||
|
class IOException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
7
core/class/sleekDB/Exceptions/IdNotAllowedException.php
Normal file
7
core/class/sleekDB/Exceptions/IdNotAllowedException.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SleekDB\Exceptions;
|
||||||
|
|
||||||
|
class IdNotAllowedException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SleekDB\Exceptions;
|
||||||
|
|
||||||
|
class InvalidArgumentException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SleekDB\Exceptions;
|
||||||
|
|
||||||
|
class InvalidConfigurationException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SleekDB\Exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class InvalidPropertyAccessException
|
||||||
|
* @deprecated since version 2.7.
|
||||||
|
*/
|
||||||
|
class InvalidPropertyAccessException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
7
core/class/sleekDB/Exceptions/JsonException.php
Normal file
7
core/class/sleekDB/Exceptions/JsonException.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SleekDB\Exceptions;
|
||||||
|
|
||||||
|
class JsonException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
192
core/class/sleekDB/Query.php
Normal file
192
core/class/sleekDB/Query.php
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SleekDB;
|
||||||
|
|
||||||
|
use SleekDB\Classes\CacheHandler;
|
||||||
|
use SleekDB\Classes\DocumentFinder;
|
||||||
|
use SleekDB\Classes\DocumentUpdater;
|
||||||
|
use SleekDB\Exceptions\InvalidArgumentException;
|
||||||
|
use SleekDB\Exceptions\IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Query
|
||||||
|
* Query execution object of SleekDB.
|
||||||
|
*/
|
||||||
|
class Query
|
||||||
|
{
|
||||||
|
|
||||||
|
const DELETE_RETURN_BOOL = 1;
|
||||||
|
const DELETE_RETURN_RESULTS = 2;
|
||||||
|
const DELETE_RETURN_COUNT = 3;
|
||||||
|
|
||||||
|
const SEARCH_ALGORITHM = [
|
||||||
|
"hits" => 1,
|
||||||
|
"hits_prioritize" => 2,
|
||||||
|
"prioritize" => 3,
|
||||||
|
"prioritize_position" => 4,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var CacheHandler|null
|
||||||
|
*/
|
||||||
|
protected $cacheHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DocumentFinder
|
||||||
|
*/
|
||||||
|
protected $documentFinder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DocumentUpdater
|
||||||
|
*/
|
||||||
|
protected $documentUpdater;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query constructor.
|
||||||
|
* @param QueryBuilder $queryBuilder
|
||||||
|
*/
|
||||||
|
public function __construct(QueryBuilder $queryBuilder)
|
||||||
|
{
|
||||||
|
$store = $queryBuilder->_getStore();
|
||||||
|
$primaryKey = $store->getPrimaryKey();
|
||||||
|
|
||||||
|
$this->cacheHandler = new CacheHandler($store->getStorePath(), $queryBuilder);
|
||||||
|
$this->documentFinder = new DocumentFinder($store->getStorePath(), $queryBuilder->_getConditionProperties(), $primaryKey);
|
||||||
|
$this->documentUpdater = new DocumentUpdater($store->getStorePath(), $primaryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute query and get results.
|
||||||
|
* @return array
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function fetch(): array
|
||||||
|
{
|
||||||
|
return $this->getResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if data is found.
|
||||||
|
* @return bool
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function exists(): bool
|
||||||
|
{
|
||||||
|
// Return boolean on data exists check.
|
||||||
|
return !empty($this->first());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the first document.
|
||||||
|
* @return array empty array or single document
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function first(): array
|
||||||
|
{
|
||||||
|
return $this->getResults(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update parts of one or multiple documents based on current query.
|
||||||
|
* @param array $updatable
|
||||||
|
* @param bool $returnUpdatedDocuments
|
||||||
|
* @return array|bool
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function update(array $updatable, bool $returnUpdatedDocuments = false){
|
||||||
|
|
||||||
|
if(empty($updatable)){
|
||||||
|
throw new InvalidArgumentException("You have to define what you want to update.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = $this->documentFinder->findDocuments(false, false);
|
||||||
|
|
||||||
|
$this->getCacheHandler()->deleteAllWithNoLifetime();
|
||||||
|
|
||||||
|
return $this->documentUpdater->updateResults($results, $updatable, $returnUpdatedDocuments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete one or multiple documents based on current query.
|
||||||
|
* @param int $returnOption
|
||||||
|
* @return bool|array|int
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function delete(int $returnOption = self::DELETE_RETURN_BOOL){
|
||||||
|
$results = $this->documentFinder->findDocuments(false, false);
|
||||||
|
|
||||||
|
$this->getCacheHandler()->deleteAllWithNoLifetime();
|
||||||
|
|
||||||
|
return $this->documentUpdater->deleteResults($results, $returnOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove fields of one or multiple documents based on current query.
|
||||||
|
* @param array $fieldsToRemove
|
||||||
|
* @return array|false
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function removeFields(array $fieldsToRemove)
|
||||||
|
{
|
||||||
|
if(empty($fieldsToRemove)){
|
||||||
|
throw new InvalidArgumentException("You have to define what fields you want to remove.");
|
||||||
|
}
|
||||||
|
$results = $this->documentFinder->findDocuments(false, false);
|
||||||
|
|
||||||
|
$this->getCacheHandler()->deleteAllWithNoLifetime();
|
||||||
|
|
||||||
|
return $this->documentUpdater->removeFields($results, $fieldsToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve Cache object.
|
||||||
|
* @return Cache
|
||||||
|
*/
|
||||||
|
public function getCache(): Cache
|
||||||
|
{
|
||||||
|
return $this->getCacheHandler()->getCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the results from either the cache or store.
|
||||||
|
* @param bool $getOneDocument
|
||||||
|
* @return array
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
private function getResults(bool $getOneDocument = false): array
|
||||||
|
{
|
||||||
|
|
||||||
|
$results = $this->getCacheHandler()->getCacheContent($getOneDocument);
|
||||||
|
|
||||||
|
if($results !== null) {
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = $this->documentFinder->findDocuments($getOneDocument, true);
|
||||||
|
|
||||||
|
if ($getOneDocument === true && count($results) > 0) {
|
||||||
|
list($item) = $results;
|
||||||
|
$results = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getCacheHandler()->setCacheContent($results);
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the caching layer bridge.
|
||||||
|
* @return CacheHandler
|
||||||
|
*/
|
||||||
|
private function getCacheHandler(): CacheHandler
|
||||||
|
{
|
||||||
|
return $this->cacheHandler;
|
||||||
|
}
|
||||||
|
}
|
516
core/class/sleekDB/QueryBuilder.php
Normal file
516
core/class/sleekDB/QueryBuilder.php
Normal file
@ -0,0 +1,516 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
578
core/class/sleekDB/SleekDB.php
Normal file
578
core/class/sleekDB/SleekDB.php
Normal file
@ -0,0 +1,578 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SleekDB;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use SleekDB\Exceptions\InvalidArgumentException;
|
||||||
|
use SleekDB\Exceptions\IdNotAllowedException;
|
||||||
|
use SleekDB\Exceptions\InvalidConfigurationException;
|
||||||
|
use SleekDB\Exceptions\IOException;
|
||||||
|
use SleekDB\Exceptions\JsonException;
|
||||||
|
|
||||||
|
if(false === class_exists("\Composer\Autoload\ClassLoader")){
|
||||||
|
require_once __DIR__.'/Store.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class SleekDB
|
||||||
|
* @package SleekDB
|
||||||
|
* @deprecated since version 2.0, use SleekDB\Store instead.
|
||||||
|
*/
|
||||||
|
class SleekDB
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var QueryBuilder
|
||||||
|
*/
|
||||||
|
protected $queryBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Store
|
||||||
|
*/
|
||||||
|
protected $store;
|
||||||
|
|
||||||
|
private $shouldKeepConditions = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SleekDB constructor.
|
||||||
|
* @param string $storeName
|
||||||
|
* @param string $dataDir
|
||||||
|
* @param array $configuration
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidConfigurationException
|
||||||
|
*/
|
||||||
|
public function __construct(string $storeName, string $dataDir, array $configuration = []){
|
||||||
|
$this->init($storeName, $dataDir, $configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the SleekDB instance.
|
||||||
|
* @param string $storeName
|
||||||
|
* @param string $dataDir
|
||||||
|
* @param array $conf
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidConfigurationException
|
||||||
|
*/
|
||||||
|
public function init(string $storeName, string $dataDir, array $conf = []){
|
||||||
|
$this->setStore(new Store($storeName, $dataDir, $conf));
|
||||||
|
$this->setQueryBuilder($this->getStore()->createQueryBuilder());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the store.
|
||||||
|
* @param string $storeName
|
||||||
|
* @param string $dataDir
|
||||||
|
* @param array $configuration
|
||||||
|
* @return SleekDB
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidConfigurationException
|
||||||
|
*/
|
||||||
|
public static function store(string $storeName, string $dataDir, array $configuration = []): SleekDB
|
||||||
|
{
|
||||||
|
return new SleekDB($storeName, $dataDir, $configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute Query and get Results
|
||||||
|
* @return array
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function fetch(): array
|
||||||
|
{
|
||||||
|
return $this->getQuery()->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if data is found
|
||||||
|
* @return bool
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function exists(): bool
|
||||||
|
{
|
||||||
|
return $this->getQuery()->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the first document.
|
||||||
|
* @return array
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function first(): array
|
||||||
|
{
|
||||||
|
return $this->getQuery()->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new object in the store.
|
||||||
|
* It is stored as a plaintext JSON document.
|
||||||
|
* @param array $storeData
|
||||||
|
* @return array
|
||||||
|
* @throws IOException
|
||||||
|
* @throws IdNotAllowedException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
public function insert(array $storeData): array
|
||||||
|
{
|
||||||
|
return $this->getStore()->insert($storeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates multiple objects in the store.
|
||||||
|
* @param array $storeData
|
||||||
|
* @return array
|
||||||
|
* @throws IOException
|
||||||
|
* @throws IdNotAllowedException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
public function insertMany(array $storeData): array
|
||||||
|
{
|
||||||
|
return $this->getStore()->insertMany($storeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update one or multiple documents, based on current query
|
||||||
|
* @param array $updatable
|
||||||
|
* @return bool
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function update(array $updatable): bool
|
||||||
|
{
|
||||||
|
return $this->getQuery()->update($updatable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes matched store objects.
|
||||||
|
* @param int $returnOption
|
||||||
|
* @return bool|array|int
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function delete(int $returnOption = Query::DELETE_RETURN_BOOL){
|
||||||
|
return $this->getQuery()->delete($returnOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a store and wipes all the data and cache it contains.
|
||||||
|
* @return bool
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function deleteStore(): bool
|
||||||
|
{
|
||||||
|
return $this->getStore()->deleteStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method would make a unique token for the current query.
|
||||||
|
* We would use this hash token as the id/name of the cache file.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCacheToken(): string
|
||||||
|
{
|
||||||
|
return $this->getQueryBuilder()->getQuery()->getCache()->getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select specific fields
|
||||||
|
* @param string[] $fieldNames
|
||||||
|
* @return SleekDB
|
||||||
|
*/
|
||||||
|
public function select(array $fieldNames): SleekDB
|
||||||
|
{
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->select($fieldNames));
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude specific fields
|
||||||
|
* @param string[] $fieldNames
|
||||||
|
* @return SleekDB
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function except(array $fieldNames): SleekDB
|
||||||
|
{
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->except($fieldNames));
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add conditions to filter data.
|
||||||
|
* @param string|array|mixed ...$conditions (string fieldName, string condition, mixed value) OR (array(array(string fieldName, string condition, mixed value)[, array(...)]))
|
||||||
|
* @return SleekDB
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function where(...$conditions): SleekDB
|
||||||
|
{
|
||||||
|
foreach ($conditions as $key => $arg) {
|
||||||
|
if ($key > 0) {
|
||||||
|
throw new InvalidArgumentException("Allowed: (string fieldName, string condition, mixed value) OR (array(array(string fieldName, string condition, mixed value)[, array(...)]))");
|
||||||
|
}
|
||||||
|
if (is_array($arg)) {
|
||||||
|
// parameters given as arrays for multiple "where" with "and" between each condition
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->where($arg));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (count($conditions) === 3) {
|
||||||
|
// parameters given as (string fieldName, string condition, mixed value) for a single "where"
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->where($conditions));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add "in" condition to filter data.
|
||||||
|
* @param string $fieldName
|
||||||
|
* @param array $values
|
||||||
|
* @return SleekDB
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function in(string $fieldName, array $values = []): SleekDB
|
||||||
|
{
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->in($fieldName, $values));
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add "not in" condition to filter data.
|
||||||
|
* @param string $fieldName
|
||||||
|
* @param array $values
|
||||||
|
* @return SleekDB
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function notIn(string $fieldName, array $values = []): SleekDB
|
||||||
|
{
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->notIn($fieldName, $values));
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or-where conditions to filter data.
|
||||||
|
* @param string|array|mixed ...$conditions (string fieldName, string condition, mixed value) OR array(array(string fieldName, string condition, mixed value) [, array(...)])
|
||||||
|
* @return SleekDB
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function orWhere(...$conditions): SleekDB
|
||||||
|
{
|
||||||
|
foreach ($conditions as $key => $arg) {
|
||||||
|
if ($key > 0) {
|
||||||
|
throw new InvalidArgumentException("Allowed: (string fieldName, string condition, mixed value) OR array(array(string fieldName, string condition, mixed value) [, array(...)])");
|
||||||
|
}
|
||||||
|
if (is_array($arg)) {
|
||||||
|
// parameters given as arrays for an "or where" with "and" between each condition
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->orWhere($arg));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (count($conditions) === 3) {
|
||||||
|
// parameters given as (string fieldName, string condition, mixed value) for a single "or where"
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->orWhere($conditions));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the amount of data record to skip.
|
||||||
|
* @param int $skip
|
||||||
|
* @return SleekDB
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function skip(int $skip = 0): SleekDB
|
||||||
|
{
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->skip($skip));
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the amount of data record to limit.
|
||||||
|
* @param int $limit
|
||||||
|
* @return SleekDB
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function limit(int $limit = 0): SleekDB
|
||||||
|
{
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->limit($limit));
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the sort order.
|
||||||
|
* @param string $order "asc" or "desc"
|
||||||
|
* @param string $orderBy
|
||||||
|
* @return SleekDB
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function orderBy(string $order, string $orderBy = '_id'): SleekDB
|
||||||
|
{
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->orderBy([$orderBy => $order]));
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a fulltext like search against more than one field.
|
||||||
|
* @param string|array $field one fieldName or multiple fieldNames as an array
|
||||||
|
* @param string $keyword
|
||||||
|
* @return SleekDB
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function search($field, string $keyword): SleekDB
|
||||||
|
{
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->search($field, $keyword));
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Closure $joinedStore
|
||||||
|
* @param string $dataPropertyName
|
||||||
|
* @return SleekDB
|
||||||
|
*/
|
||||||
|
public function join(Closure $joinedStore, string $dataPropertyName): SleekDB
|
||||||
|
{
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->join($joinedStore, $dataPropertyName));
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-generate the cache for the query.
|
||||||
|
* @return SleekDB
|
||||||
|
*/
|
||||||
|
public function makeCache(): SleekDB
|
||||||
|
{
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->regenerateCache());
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable cache for the query.
|
||||||
|
* @return SleekDB
|
||||||
|
*/
|
||||||
|
public function disableCache(): SleekDB
|
||||||
|
{
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->disableCache());
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use caching for current query
|
||||||
|
* @param int|null $lifetime time to live as int in seconds or null to regenerate cache on every insert, update and delete
|
||||||
|
* @return SleekDB
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function useCache(int $lifetime = null): SleekDB
|
||||||
|
{
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->useCache($lifetime));
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete cache file/s for current query.
|
||||||
|
* @return SleekDB
|
||||||
|
*/
|
||||||
|
public function deleteCache(): SleekDB
|
||||||
|
{
|
||||||
|
$this->getQueryBuilder()->getQuery()->getCache()->delete();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all cache files for current store.
|
||||||
|
* @return SleekDB
|
||||||
|
*/
|
||||||
|
public function deleteAllCache(): SleekDB
|
||||||
|
{
|
||||||
|
$this->getQueryBuilder()->getQuery()->getCache()->deleteAll();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep the active query conditions.
|
||||||
|
* @return SleekDB
|
||||||
|
*/
|
||||||
|
public function keepConditions(): SleekDB
|
||||||
|
{
|
||||||
|
$this->shouldKeepConditions = true;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return distinct values.
|
||||||
|
* @param array|string $fields
|
||||||
|
* @return SleekDB
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function distinct($fields = []): SleekDB
|
||||||
|
{
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->distinct($fields));
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return QueryBuilder
|
||||||
|
*/
|
||||||
|
public function getQueryBuilder(): QueryBuilder
|
||||||
|
{
|
||||||
|
return $this->queryBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param QueryBuilder $queryBuilder
|
||||||
|
*/
|
||||||
|
private function setQueryBuilder(QueryBuilder $queryBuilder){
|
||||||
|
$this->queryBuilder = $queryBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Query
|
||||||
|
*/
|
||||||
|
public function getQuery(): Query
|
||||||
|
{
|
||||||
|
$query = $this->getQueryBuilder()->getQuery();
|
||||||
|
$this->resetQueryBuilder();
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Cache
|
||||||
|
*/
|
||||||
|
public function getCache(): Cache
|
||||||
|
{
|
||||||
|
// we do not want to reset the QueryBuilder
|
||||||
|
return $this->getQueryBuilder()->getQuery()->getCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Store $store
|
||||||
|
*/
|
||||||
|
private function setStore(Store $store){
|
||||||
|
$this->store = $store;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Store
|
||||||
|
*/
|
||||||
|
public function getStore(): Store
|
||||||
|
{
|
||||||
|
return $this->store;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle shouldKeepConditions and reset queryBuilder accordingly
|
||||||
|
*/
|
||||||
|
private function resetQueryBuilder(){
|
||||||
|
if($this->shouldKeepConditions === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->setQueryBuilder($this->getStore()->createQueryBuilder());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all documents.
|
||||||
|
* @return array
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function findAll(): array
|
||||||
|
{
|
||||||
|
return $this->getStore()->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve one document by its _id. Very fast because it finds the document by its file path.
|
||||||
|
* @param int $id
|
||||||
|
* @return array|null
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function findById(int $id){
|
||||||
|
return $this->getStore()->findById($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve one or multiple documents.
|
||||||
|
* @param array $criteria
|
||||||
|
* @param array $orderBy
|
||||||
|
* @param int $limit
|
||||||
|
* @param int $offset
|
||||||
|
* @return array
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array
|
||||||
|
{
|
||||||
|
return $this->getStore()->findBy($criteria, $orderBy, $limit, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve one document.
|
||||||
|
* @param array $criteria
|
||||||
|
* @return array|null single document or NULL if no document can be found
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function findOneBy(array $criteria)
|
||||||
|
{
|
||||||
|
return $this->getStore()->findOneBy($criteria);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update one or multiple documents.
|
||||||
|
* @param array $updatable true if all documents could be updated and false if one document did not exist
|
||||||
|
* @return bool
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function updateBy(array $updatable): bool
|
||||||
|
{
|
||||||
|
return $this->getStore()->update($updatable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete one or multiple documents.
|
||||||
|
* @param $criteria
|
||||||
|
* @param int $returnOption
|
||||||
|
* @return array|bool|int
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function deleteBy($criteria, $returnOption = Query::DELETE_RETURN_BOOL){
|
||||||
|
return $this->getStore()->deleteBy($criteria, $returnOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete one document by its _id. Very fast because it deletes the document by its file path.
|
||||||
|
* @param $id
|
||||||
|
* @return bool true if document does not exist or deletion was successful, false otherwise
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function deleteById($id): bool
|
||||||
|
{
|
||||||
|
return $this->getStore()->deleteById($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a where statement that is nested. ( $x or ($y and $z) )
|
||||||
|
* @param array $conditions
|
||||||
|
* @return $this
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @deprecated since version 2.3, use where and orWhere instead.
|
||||||
|
*/
|
||||||
|
public function nestedWhere(array $conditions): SleekDB
|
||||||
|
{
|
||||||
|
$this->setQueryBuilder($this->getQueryBuilder()->nestedWhere($conditions));
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
888
core/class/sleekDB/Store.php
Normal file
888
core/class/sleekDB/Store.php
Normal file
@ -0,0 +1,888 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SleekDB;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use SleekDB\Classes\IoHelper;
|
||||||
|
use SleekDB\Classes\NestedHelper;
|
||||||
|
use SleekDB\Exceptions\InvalidArgumentException;
|
||||||
|
use SleekDB\Exceptions\IdNotAllowedException;
|
||||||
|
use SleekDB\Exceptions\InvalidConfigurationException;
|
||||||
|
use SleekDB\Exceptions\IOException;
|
||||||
|
use SleekDB\Exceptions\JsonException;
|
||||||
|
|
||||||
|
// To provide usage without composer, we need to require all files.
|
||||||
|
if(false === class_exists("\Composer\Autoload\ClassLoader")) {
|
||||||
|
foreach (glob(__DIR__ . '/Exceptions/*.php') as $exception) {
|
||||||
|
require_once $exception;
|
||||||
|
}
|
||||||
|
foreach (glob(__DIR__ . '/Classes/*.php') as $traits) {
|
||||||
|
require_once $traits;
|
||||||
|
}
|
||||||
|
foreach (glob(__DIR__ . '/*.php') as $class) {
|
||||||
|
if (strpos($class, 'SleekDB.php') !== false || strpos($class, 'Store.php') !== false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
require_once $class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Store
|
||||||
|
{
|
||||||
|
|
||||||
|
protected $root = __DIR__;
|
||||||
|
|
||||||
|
protected $storeName = "";
|
||||||
|
protected $storePath = "";
|
||||||
|
|
||||||
|
protected $databasePath = "";
|
||||||
|
|
||||||
|
protected $useCache = true;
|
||||||
|
protected $defaultCacheLifetime;
|
||||||
|
protected $primaryKey = "_id";
|
||||||
|
protected $timeout = 120;
|
||||||
|
protected $searchOptions = [
|
||||||
|
"minLength" => 2,
|
||||||
|
"scoreKey" => "searchScore",
|
||||||
|
"mode" => "or",
|
||||||
|
"algorithm" => Query::SEARCH_ALGORITHM["hits"]
|
||||||
|
];
|
||||||
|
|
||||||
|
const dataDirectory = "data/";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store constructor.
|
||||||
|
* @param string $storeName
|
||||||
|
* @param string $databasePath
|
||||||
|
* @param array $configuration
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidConfigurationException
|
||||||
|
*/
|
||||||
|
public function __construct(string $storeName, string $databasePath, array $configuration = [])
|
||||||
|
{
|
||||||
|
$storeName = trim($storeName);
|
||||||
|
if (empty($storeName)) {
|
||||||
|
throw new InvalidArgumentException('store name can not be empty');
|
||||||
|
}
|
||||||
|
$this->storeName = $storeName;
|
||||||
|
|
||||||
|
$databasePath = trim($databasePath);
|
||||||
|
if (empty($databasePath)) {
|
||||||
|
throw new InvalidArgumentException('data directory can not be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
IoHelper::normalizeDirectory($databasePath);
|
||||||
|
$this->databasePath = $databasePath;
|
||||||
|
|
||||||
|
$this->setConfiguration($configuration);
|
||||||
|
|
||||||
|
// boot store
|
||||||
|
$this->createDatabasePath();
|
||||||
|
$this->createStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the destination of the store object.
|
||||||
|
* @param string $storeName
|
||||||
|
* @param string|null $databasePath If empty, previous database path will be used.
|
||||||
|
* @param array $configuration
|
||||||
|
* @return Store
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws InvalidConfigurationException
|
||||||
|
*/
|
||||||
|
public function changeStore(string $storeName, string $databasePath = null, array $configuration = []): Store
|
||||||
|
{
|
||||||
|
if(empty($databasePath)){
|
||||||
|
$databasePath = $this->getDatabasePath();
|
||||||
|
}
|
||||||
|
$this->__construct($storeName, $databasePath, $configuration);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getStoreName(): string
|
||||||
|
{
|
||||||
|
return $this->storeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getDatabasePath(): string
|
||||||
|
{
|
||||||
|
return $this->databasePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return QueryBuilder
|
||||||
|
*/
|
||||||
|
public function createQueryBuilder(): QueryBuilder
|
||||||
|
{
|
||||||
|
return new QueryBuilder($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new document to the store.
|
||||||
|
* It is stored as a plaintext JSON document.
|
||||||
|
* @param array $data
|
||||||
|
* @return array inserted document
|
||||||
|
* @throws IOException
|
||||||
|
* @throws IdNotAllowedException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
public function insert(array $data): array
|
||||||
|
{
|
||||||
|
// Handle invalid data
|
||||||
|
if (empty($data)) {
|
||||||
|
throw new InvalidArgumentException('No data found to insert in the store');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->writeNewDocumentToStore($data);
|
||||||
|
|
||||||
|
$this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert multiple documents to the store.
|
||||||
|
* They are stored as plaintext JSON documents.
|
||||||
|
* @param array $data
|
||||||
|
* @return array inserted documents
|
||||||
|
* @throws IOException
|
||||||
|
* @throws IdNotAllowedException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
public function insertMany(array $data): array
|
||||||
|
{
|
||||||
|
// Handle invalid data
|
||||||
|
if (empty($data)) {
|
||||||
|
throw new InvalidArgumentException('No data found to insert in the store');
|
||||||
|
}
|
||||||
|
|
||||||
|
// All results.
|
||||||
|
$results = [];
|
||||||
|
foreach ($data as $document) {
|
||||||
|
$results[] = $this->writeNewDocumentToStore($document);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete store with all its data and cache.
|
||||||
|
* @return bool
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function deleteStore(): bool
|
||||||
|
{
|
||||||
|
$storePath = $this->getStorePath();
|
||||||
|
return IoHelper::deleteFolder($storePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the last created store object ID.
|
||||||
|
* @return int
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function getLastInsertedId(): int
|
||||||
|
{
|
||||||
|
$counterPath = $this->getStorePath() . '_cnt.sdb';
|
||||||
|
|
||||||
|
return (int) IoHelper::getFileContent($counterPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getStorePath(): string
|
||||||
|
{
|
||||||
|
return $this->storePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all documents.
|
||||||
|
* @param array|null $orderBy array($fieldName => $order). $order can be "asc" or "desc"
|
||||||
|
* @param int|null $limit the amount of data record to limit
|
||||||
|
* @param int|null $offset the amount of data record to skip
|
||||||
|
* @return array
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function findAll(array $orderBy = null, int $limit = null, int $offset = null): array
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder();
|
||||||
|
if(!is_null($orderBy)){
|
||||||
|
$qb->orderBy($orderBy);
|
||||||
|
}
|
||||||
|
if(!is_null($limit)){
|
||||||
|
$qb->limit($limit);
|
||||||
|
}
|
||||||
|
if(!is_null($offset)){
|
||||||
|
$qb->skip($offset);
|
||||||
|
}
|
||||||
|
return $qb->getQuery()->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve one document by its primary key. Very fast because it finds the document by its file path.
|
||||||
|
* @param int|string $id
|
||||||
|
* @return array|null
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function findById($id){
|
||||||
|
|
||||||
|
$id = $this->checkAndStripId($id);
|
||||||
|
|
||||||
|
$filePath = $this->getDataPath() . "$id.json";
|
||||||
|
|
||||||
|
try{
|
||||||
|
$content = IoHelper::getFileContent($filePath);
|
||||||
|
} catch (Exception $exception){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return @json_decode($content, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve one or multiple documents.
|
||||||
|
* @param array $criteria
|
||||||
|
* @param array $orderBy
|
||||||
|
* @param int $limit
|
||||||
|
* @param int $offset
|
||||||
|
* @return array
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder();
|
||||||
|
|
||||||
|
$qb->where($criteria);
|
||||||
|
|
||||||
|
if($orderBy !== null) {
|
||||||
|
$qb->orderBy($orderBy);
|
||||||
|
}
|
||||||
|
if($limit !== null) {
|
||||||
|
$qb->limit($limit);
|
||||||
|
}
|
||||||
|
if($offset !== null) {
|
||||||
|
$qb->skip($offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb->getQuery()->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve one document.
|
||||||
|
* @param array $criteria
|
||||||
|
* @return array|null single document or NULL if no document can be found
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function findOneBy(array $criteria)
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder();
|
||||||
|
|
||||||
|
$qb->where($criteria);
|
||||||
|
|
||||||
|
$result = $qb->getQuery()->first();
|
||||||
|
|
||||||
|
return (!empty($result))? $result : null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update or insert one document.
|
||||||
|
* @param array $data
|
||||||
|
* @param bool $autoGenerateIdOnInsert
|
||||||
|
* @return array updated / inserted document
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
public function updateOrInsert(array $data, bool $autoGenerateIdOnInsert = true): array
|
||||||
|
{
|
||||||
|
$primaryKey = $this->getPrimaryKey();
|
||||||
|
|
||||||
|
if(empty($data)) {
|
||||||
|
throw new InvalidArgumentException("No document to update or insert.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// // we can use this check to determine if multiple documents are given
|
||||||
|
// // because documents have to have at least the primary key.
|
||||||
|
// if(array_keys($data) !== range(0, (count($data) - 1))){
|
||||||
|
// $data = [ $data ];
|
||||||
|
// }
|
||||||
|
|
||||||
|
if(!array_key_exists($primaryKey, $data)) {
|
||||||
|
// $documentString = var_export($document, true);
|
||||||
|
// throw new InvalidArgumentException("Documents have to have the primary key \"$primaryKey\". Got data: $documentString");
|
||||||
|
$data[$primaryKey] = $this->increaseCounterAndGetNextId();
|
||||||
|
} else {
|
||||||
|
$data[$primaryKey] = $this->checkAndStripId($data[$primaryKey]);
|
||||||
|
if($autoGenerateIdOnInsert && $this->findById($data[$primaryKey]) === null){
|
||||||
|
$data[$primaryKey] = $this->increaseCounterAndGetNextId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One document to update or insert
|
||||||
|
|
||||||
|
// save to access file with primary key value because we secured it above
|
||||||
|
$storePath = $this->getDataPath() . "$data[$primaryKey].json";
|
||||||
|
IoHelper::writeContentToFile($storePath, json_encode($data));
|
||||||
|
|
||||||
|
$this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update or insert multiple documents.
|
||||||
|
* @param array $data
|
||||||
|
* @param bool $autoGenerateIdOnInsert
|
||||||
|
* @return array updated / inserted documents
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
public function updateOrInsertMany(array $data, bool $autoGenerateIdOnInsert = true): array
|
||||||
|
{
|
||||||
|
$primaryKey = $this->getPrimaryKey();
|
||||||
|
|
||||||
|
if(empty($data)) {
|
||||||
|
throw new InvalidArgumentException("No documents to update or insert.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// // we can use this check to determine if multiple documents are given
|
||||||
|
// // because documents have to have at least the primary key.
|
||||||
|
// if(array_keys($data) !== range(0, (count($data) - 1))){
|
||||||
|
// $data = [ $data ];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Check if all documents have the primary key before updating or inserting any
|
||||||
|
foreach ($data as $key => $document){
|
||||||
|
if(!is_array($document)) {
|
||||||
|
throw new InvalidArgumentException('Documents have to be an arrays.');
|
||||||
|
}
|
||||||
|
if(!array_key_exists($primaryKey, $document)) {
|
||||||
|
// $documentString = var_export($document, true);
|
||||||
|
// throw new InvalidArgumentException("Documents have to have the primary key \"$primaryKey\". Got data: $documentString");
|
||||||
|
$document[$primaryKey] = $this->increaseCounterAndGetNextId();
|
||||||
|
} else {
|
||||||
|
$document[$primaryKey] = $this->checkAndStripId($document[$primaryKey]);
|
||||||
|
if($autoGenerateIdOnInsert && $this->findById($document[$primaryKey]) === null){
|
||||||
|
$document[$primaryKey] = $this->increaseCounterAndGetNextId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// after the stripping and checking we apply it back
|
||||||
|
$data[$key] = $document;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One or multiple documents to update or insert
|
||||||
|
foreach ($data as $document) {
|
||||||
|
// save to access file with primary key value because we secured it above
|
||||||
|
$storePath = $this->getDataPath() . "$document[$primaryKey].json";
|
||||||
|
IoHelper::writeContentToFile($storePath, json_encode($document));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update one or multiple documents.
|
||||||
|
* @param array $updatable
|
||||||
|
* @return bool true if all documents could be updated and false if one document did not exist
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function update(array $updatable): bool
|
||||||
|
{
|
||||||
|
$primaryKey = $this->getPrimaryKey();
|
||||||
|
|
||||||
|
if(empty($updatable)) {
|
||||||
|
throw new InvalidArgumentException("No documents to update.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// we can use this check to determine if multiple documents are given
|
||||||
|
// because documents have to have at least the primary key.
|
||||||
|
if(array_keys($updatable) !== range(0, (count($updatable) - 1))){
|
||||||
|
$updatable = [ $updatable ];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all documents exist and have the primary key before updating any
|
||||||
|
foreach ($updatable as $key => $document){
|
||||||
|
if(!is_array($document)) {
|
||||||
|
throw new InvalidArgumentException('Documents have to be an arrays.');
|
||||||
|
}
|
||||||
|
if(!array_key_exists($primaryKey, $document)) {
|
||||||
|
throw new InvalidArgumentException("Documents have to have the primary key \"$primaryKey\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
$document[$primaryKey] = $this->checkAndStripId($document[$primaryKey]);
|
||||||
|
// after the stripping and checking we apply it back to the updatable array.
|
||||||
|
$updatable[$key] = $document;
|
||||||
|
|
||||||
|
$storePath = $this->getDataPath() . "$document[$primaryKey].json";
|
||||||
|
|
||||||
|
if (!file_exists($storePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One or multiple documents to update
|
||||||
|
foreach ($updatable as $document) {
|
||||||
|
// save to access file with primary key value because we secured it above
|
||||||
|
$storePath = $this->getDataPath() . "$document[$primaryKey].json";
|
||||||
|
IoHelper::writeContentToFile($storePath, json_encode($document));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update properties of one document.
|
||||||
|
* @param int|string $id
|
||||||
|
* @param array $updatable
|
||||||
|
* @return array|false Updated document or false if document does not exist.
|
||||||
|
* @throws IOException If document could not be read or written.
|
||||||
|
* @throws InvalidArgumentException If one key to update is primary key or $id is not int or string.
|
||||||
|
* @throws JsonException If content of document file could not be decoded.
|
||||||
|
*/
|
||||||
|
public function updateById($id, array $updatable)
|
||||||
|
{
|
||||||
|
|
||||||
|
$id = $this->checkAndStripId($id);
|
||||||
|
|
||||||
|
$filePath = $this->getDataPath() . "$id.json";
|
||||||
|
|
||||||
|
$primaryKey = $this->getPrimaryKey();
|
||||||
|
|
||||||
|
if(array_key_exists($primaryKey, $updatable)) {
|
||||||
|
throw new InvalidArgumentException("You can not update the primary key \"$primaryKey\" of documents.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!file_exists($filePath)){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = IoHelper::updateFileContent($filePath, function($content) use ($filePath, $updatable){
|
||||||
|
$content = @json_decode($content, true);
|
||||||
|
if(!is_array($content)){
|
||||||
|
throw new JsonException("Could not decode content of \"$filePath\" with json_decode.");
|
||||||
|
}
|
||||||
|
foreach ($updatable as $key => $value){
|
||||||
|
NestedHelper::updateNestedValue($key, $content, $value);
|
||||||
|
}
|
||||||
|
return json_encode($content);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
|
||||||
|
|
||||||
|
return json_decode($content, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete one or multiple documents.
|
||||||
|
* @param array $criteria
|
||||||
|
* @param int $returnOption
|
||||||
|
* @return array|bool|int
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function deleteBy(array $criteria, int $returnOption = Query::DELETE_RETURN_BOOL){
|
||||||
|
|
||||||
|
$query = $this->createQueryBuilder()->where($criteria)->getQuery();
|
||||||
|
|
||||||
|
$query->getCache()->deleteAllWithNoLifetime();
|
||||||
|
|
||||||
|
return $query->delete($returnOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete one document by its primary key. Very fast because it deletes the document by its file path.
|
||||||
|
* @param int|string $id
|
||||||
|
* @return bool true if document does not exist or deletion was successful, false otherwise
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function deleteById($id): bool
|
||||||
|
{
|
||||||
|
|
||||||
|
$id = $this->checkAndStripId($id);
|
||||||
|
|
||||||
|
$filePath = $this->getDataPath() . "$id.json";
|
||||||
|
|
||||||
|
$this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
|
||||||
|
|
||||||
|
return (!file_exists($filePath) || true === @unlink($filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove fields from one document by its primary key.
|
||||||
|
* @param int|string $id
|
||||||
|
* @param array $fieldsToRemove
|
||||||
|
* @return false|array
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
public function removeFieldsById($id, array $fieldsToRemove)
|
||||||
|
{
|
||||||
|
$id = $this->checkAndStripId($id);
|
||||||
|
$filePath = $this->getDataPath() . "$id.json";
|
||||||
|
$primaryKey = $this->getPrimaryKey();
|
||||||
|
|
||||||
|
if(in_array($primaryKey, $fieldsToRemove, false)) {
|
||||||
|
throw new InvalidArgumentException("You can not remove the primary key \"$primaryKey\" of documents.");
|
||||||
|
}
|
||||||
|
if(!file_exists($filePath)){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = IoHelper::updateFileContent($filePath, function($content) use ($filePath, $fieldsToRemove){
|
||||||
|
$content = @json_decode($content, true);
|
||||||
|
if(!is_array($content)){
|
||||||
|
throw new JsonException("Could not decode content of \"$filePath\" with json_decode.");
|
||||||
|
}
|
||||||
|
foreach ($fieldsToRemove as $fieldToRemove){
|
||||||
|
NestedHelper::removeNestedField($content, $fieldToRemove);
|
||||||
|
}
|
||||||
|
return $content;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
|
||||||
|
|
||||||
|
return json_decode($content, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a fulltext like search against one or multiple fields.
|
||||||
|
* @param array $fields
|
||||||
|
* @param string $query
|
||||||
|
* @param array|null $orderBy
|
||||||
|
* @param int|null $limit
|
||||||
|
* @param int|null $offset
|
||||||
|
* @return array
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function search(array $fields, string $query, array $orderBy = null, int $limit = null, int $offset = null): array
|
||||||
|
{
|
||||||
|
|
||||||
|
$qb = $this->createQueryBuilder();
|
||||||
|
|
||||||
|
$qb->search($fields, $query);
|
||||||
|
|
||||||
|
if($orderBy !== null) {
|
||||||
|
$qb->orderBy($orderBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($limit !== null) {
|
||||||
|
$qb->limit($limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($offset !== null) {
|
||||||
|
$qb->skip($offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb->getQuery()->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the field used as the primary key.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getPrimaryKey(): string
|
||||||
|
{
|
||||||
|
return $this->primaryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the amount of documents in the store.
|
||||||
|
* @return int
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public function count(): int
|
||||||
|
{
|
||||||
|
if($this->_getUseCache() === true){
|
||||||
|
$cacheTokenArray = ["count" => true];
|
||||||
|
$cache = new Cache($this->getStorePath(), $cacheTokenArray, null);
|
||||||
|
$cacheValue = $cache->get();
|
||||||
|
if(is_array($cacheValue) && array_key_exists("count", $cacheValue)){
|
||||||
|
return $cacheValue["count"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$value = [
|
||||||
|
"count" => IoHelper::countFolderContent($this->getDataPath())
|
||||||
|
];
|
||||||
|
if(isset($cache)) {
|
||||||
|
$cache->set($value);
|
||||||
|
}
|
||||||
|
return $value["count"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the search options of the store.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function _getSearchOptions(): array
|
||||||
|
{
|
||||||
|
return $this->searchOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if caching is enabled store wide.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function _getUseCache(): bool
|
||||||
|
{
|
||||||
|
return $this->useCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the store wide default cache lifetime.
|
||||||
|
* @return null|int
|
||||||
|
*/
|
||||||
|
public function _getDefaultCacheLifetime()
|
||||||
|
{
|
||||||
|
return $this->defaultCacheLifetime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
* @deprecated since version 2.7, use getDatabasePath instead.
|
||||||
|
*/
|
||||||
|
public function getDataDirectory(): string
|
||||||
|
{
|
||||||
|
// TODO remove with version 3.0
|
||||||
|
return $this->databasePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private function createDatabasePath()
|
||||||
|
{
|
||||||
|
$databasePath = $this->getDatabasePath();
|
||||||
|
IoHelper::createFolder($databasePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private function createStore()
|
||||||
|
{
|
||||||
|
$storeName = $this->getStoreName();
|
||||||
|
// Prepare store name.
|
||||||
|
IoHelper::normalizeDirectory($storeName);
|
||||||
|
// Store directory path.
|
||||||
|
$this->storePath = $this->getDatabasePath() . $storeName;
|
||||||
|
$storePath = $this->getStorePath();
|
||||||
|
IoHelper::createFolder($storePath);
|
||||||
|
|
||||||
|
// Create the cache directory.
|
||||||
|
$cacheDirectory = $storePath . 'cache';
|
||||||
|
IoHelper::createFolder($cacheDirectory);
|
||||||
|
|
||||||
|
// Create the data directory.
|
||||||
|
IoHelper::createFolder($storePath . self::dataDirectory);
|
||||||
|
|
||||||
|
// Create the store counter file.
|
||||||
|
$counterFile = $storePath . '_cnt.sdb';
|
||||||
|
if(!file_exists($counterFile)){
|
||||||
|
IoHelper::writeContentToFile($counterFile, '0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $configuration
|
||||||
|
* @throws InvalidConfigurationException
|
||||||
|
*/
|
||||||
|
private function setConfiguration(array $configuration)
|
||||||
|
{
|
||||||
|
if(array_key_exists("auto_cache", $configuration)){
|
||||||
|
$autoCache = $configuration["auto_cache"];
|
||||||
|
if(!is_bool($configuration["auto_cache"])){
|
||||||
|
throw new InvalidConfigurationException("auto_cache has to be boolean");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->useCache = $autoCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(array_key_exists("cache_lifetime", $configuration)){
|
||||||
|
$defaultCacheLifetime = $configuration["cache_lifetime"];
|
||||||
|
if(!is_int($defaultCacheLifetime) && !is_null($defaultCacheLifetime)){
|
||||||
|
throw new InvalidConfigurationException("cache_lifetime has to be null or int");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->defaultCacheLifetime = $defaultCacheLifetime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO remove timeout on major update
|
||||||
|
// Set timeout.
|
||||||
|
if (array_key_exists("timeout", $configuration)) {
|
||||||
|
if ((!is_int($configuration['timeout']) || $configuration['timeout'] <= 0) && !($configuration['timeout'] === false)){
|
||||||
|
throw new InvalidConfigurationException("timeout has to be an int > 0 or false");
|
||||||
|
}
|
||||||
|
$this->timeout = $configuration["timeout"];
|
||||||
|
}
|
||||||
|
if($this->timeout !== false){
|
||||||
|
$message = 'The "timeout" configuration is deprecated and will be removed with the next major update.' .
|
||||||
|
' Set the "timeout" configuration to false and if needed use the set_timeout_limit() function in your own code.';
|
||||||
|
trigger_error($message, E_USER_DEPRECATED);
|
||||||
|
set_time_limit($this->timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(array_key_exists("primary_key", $configuration)){
|
||||||
|
$primaryKey = $configuration["primary_key"];
|
||||||
|
if(!is_string($primaryKey)){
|
||||||
|
throw new InvalidConfigurationException("primary key has to be a string");
|
||||||
|
}
|
||||||
|
$this->primaryKey = $primaryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(array_key_exists("search", $configuration)){
|
||||||
|
$searchConfig = $configuration["search"];
|
||||||
|
|
||||||
|
if(array_key_exists("min_length", $searchConfig)){
|
||||||
|
$searchMinLength = $searchConfig["min_length"];
|
||||||
|
if(!is_int($searchMinLength) || $searchMinLength <= 0){
|
||||||
|
throw new InvalidConfigurationException("min length for searching has to be an int >= 0");
|
||||||
|
}
|
||||||
|
$this->searchOptions["minLength"] = $searchMinLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(array_key_exists("mode", $searchConfig)){
|
||||||
|
$searchMode = $searchConfig["mode"];
|
||||||
|
if(!is_string($searchMode) || !in_array(strtolower(trim($searchMode)), ["and", "or"])){
|
||||||
|
throw new InvalidConfigurationException("search mode can just be \"and\" or \"or\"");
|
||||||
|
}
|
||||||
|
$this->searchOptions["mode"] = strtolower(trim($searchMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(array_key_exists("score_key", $searchConfig)){
|
||||||
|
$searchScoreKey = $searchConfig["score_key"];
|
||||||
|
if((!is_string($searchScoreKey) && !is_null($searchScoreKey))){
|
||||||
|
throw new InvalidConfigurationException("search score key for search has to be a not empty string or null");
|
||||||
|
}
|
||||||
|
$this->searchOptions["scoreKey"] = $searchScoreKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(array_key_exists("algorithm", $searchConfig)){
|
||||||
|
$searchAlgorithm = $searchConfig["algorithm"];
|
||||||
|
if(!in_array($searchAlgorithm, Query::SEARCH_ALGORITHM, true)){
|
||||||
|
$searchAlgorithm = implode(', ', $searchAlgorithm);
|
||||||
|
throw new InvalidConfigurationException("The search algorithm has to be one of the following integer values ($searchAlgorithm)");
|
||||||
|
}
|
||||||
|
$this->searchOptions["algorithm"] = $searchAlgorithm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes an object in a store.
|
||||||
|
* @param array $storeData
|
||||||
|
* @return array
|
||||||
|
* @throws IOException
|
||||||
|
* @throws IdNotAllowedException
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
private function writeNewDocumentToStore(array $storeData): array
|
||||||
|
{
|
||||||
|
$primaryKey = $this->getPrimaryKey();
|
||||||
|
// Check if it has the primary key
|
||||||
|
if (isset($storeData[$primaryKey])) {
|
||||||
|
throw new IdNotAllowedException(
|
||||||
|
"The \"$primaryKey\" index is reserved by SleekDB, please delete the $primaryKey key and try again"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$id = $this->increaseCounterAndGetNextId();
|
||||||
|
// Add the system ID with the store data array.
|
||||||
|
$storeData[$primaryKey] = $id;
|
||||||
|
// Prepare storable data
|
||||||
|
$storableJSON = @json_encode($storeData);
|
||||||
|
if ($storableJSON === false) {
|
||||||
|
throw new JsonException('Unable to encode the data array,
|
||||||
|
please provide a valid PHP associative array');
|
||||||
|
}
|
||||||
|
// Define the store path
|
||||||
|
$filePath = $this->getDataPath()."$id.json";
|
||||||
|
|
||||||
|
IoHelper::writeContentToFile($filePath, $storableJSON);
|
||||||
|
|
||||||
|
return $storeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments the store wide unique store object ID and returns it.
|
||||||
|
* @return int
|
||||||
|
* @throws IOException
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
private function increaseCounterAndGetNextId(): int
|
||||||
|
{
|
||||||
|
$counterPath = $this->getStorePath() . '_cnt.sdb';
|
||||||
|
|
||||||
|
if (!file_exists($counterPath)) {
|
||||||
|
throw new IOException("File $counterPath does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$dataPath = $this->getDataPath();
|
||||||
|
|
||||||
|
return (int) IoHelper::updateFileContent($counterPath, function ($counter) use ($dataPath){
|
||||||
|
$newCounter = ((int) $counter) + 1;
|
||||||
|
|
||||||
|
while(file_exists($dataPath."$newCounter.json") === true){
|
||||||
|
$newCounter++;
|
||||||
|
}
|
||||||
|
return (string)$newCounter;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|int $id
|
||||||
|
* @return int
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
private function checkAndStripId($id): int
|
||||||
|
{
|
||||||
|
if(!is_string($id) && !is_int($id)){
|
||||||
|
throw new InvalidArgumentException("The id of the document has to be an integer or string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(is_string($id)){
|
||||||
|
$id = IoHelper::secureStringForFileAccess($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!is_numeric($id)){
|
||||||
|
throw new InvalidArgumentException("The id of the document has to be numeric");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getDataPath(): string
|
||||||
|
{
|
||||||
|
return $this->getStorePath() . self::dataDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user