dataDirectory = $dataDir; // Set auto cache settings. $autoCache = true; if ( isset( $conf[ 'auto_cache' ] ) ) $autoCache = $conf[ 'auto_cache' ]; $this->initAutoCache( $autoCache ); // Set timeout. $timeout = 120; if ( isset( $conf[ 'timeout' ] ) ) { if ( !empty( $conf[ 'timeout' ] ) ) $timeout = (int) $conf[ 'timeout' ]; } set_time_limit( $timeout ); // Control when to keep or delete the active query conditions. Delete conditions by default. $this->shouldKeepConditions = false; } // End of init() /** * Init data that SleekDB required to operate. */ private function initVariables() { if(!$this->shouldKeepConditions) { // Set empty results $this->results = []; // Set a default limit $this->limit = 0; // Set a default skip $this->skip = 0; // Set default conditions $this->conditions = []; // Or conditions $this->orConditions = []; // In clause conditions $this->in = []; // notIn clause conditions $this->notIn = []; // Set default group by value $this->orderBy = [ 'order' => false, 'field' => '_id' ]; // Set the default search keyword as an empty string. $this->searchKeyword = ''; // Disable make cache by default. $this->makeCache = false; // Control when to keep or delete the active query conditions. Delete conditions by default. $this->shouldKeepConditions = false; // specific fields to select $this->fieldsToSelect = []; $this->fieldsToExclude = []; $this->orConditionsWithAnd = []; } } // End of initVariables() /** * Initialize the auto cache settings. * @param bool $autoCache */ private function initAutoCache ( $autoCache = true ) { // Decide the cache status. if ( $autoCache === true ) { $this->useCache = true; // A flag that is used to check if cache should be empty // while create a new object in a store. $this->deleteCacheOnCreate = true; } else { $this->useCache = false; // A flag that is used to check if cache should be empty // while create a new object in a store. $this->deleteCacheOnCreate = false; } } /** * Method to boot a store. * @throws EmptyStoreNameException * @throws IOException */ private function bootStore() { $store = trim( $this->storeName ); // Validate the store name. if ( !$store || empty( $store ) ) throw new EmptyStoreNameException( 'Invalid store name was found' ); // Prepare store name. if ( substr( $store, -1 ) !== '/' ) $store = $store . '/'; // Store directory path. $this->storePath = $this->dataDirectory . $store; // Check if the store exists. if ( !file_exists( $this->storePath ) ) { // The directory was not found, create one with cache directory. if ( !mkdir( $this->storePath, 0777, true ) ) throw new IOException( 'Unable to create the store path at ' . $this->storePath ); // Create the cache directory. if ( !mkdir( $this->storePath . 'cache', 0777, true ) ) throw new IOException( 'Unable to create the store\'s cache directory at ' . $this->storePath . 'cache' ); // Create the data directory. if ( !mkdir( $this->storePath . 'data', 0777, true ) ) throw new IOException( 'Unable to create the store\'s data directory at ' . $this->storePath . 'data' ); // Create the store counter file. if ( !file_put_contents( $this->storePath . '_cnt.sdb', '0' ) ) throw new IOException( 'Unable to create the system counter for the store! Please check write permission' ); } // Check if PHP has write permission in that directory. if ( !is_writable( $this->storePath ) ) throw new IOException( 'Store path is not writable at "' . $this->storePath . '." Please change store path permission.' ); // Finally check if the directory is readable by PHP. if ( !is_readable( $this->storePath ) ) throw new IOException( 'Store path is not readable at "' . $this->storePath . '." Please change store path permission.' ); } // Returns a new and unique store object ID, by calling this method it would also // increment the ID system-wide only for the store. private function getStoreId() { $counter = 1; // default (first) id $counterPath = $this->storePath . '_cnt.sdb'; if ( file_exists( $counterPath ) ) { $fp = fopen($counterPath, 'r+'); for($retries = 10; $retries > 0; $retries--) { flock($fp, LOCK_UN); if (flock($fp, LOCK_EX) === false) { sleep(1); } else { $counter = (int) fgets($fp); $counter++; rewind($fp); fwrite($fp, (string) $counter); break; } } flock($fp, LOCK_UN); fclose($fp); } return $counter; } /** * Return the last created store object ID. * @return int */ private function getLastStoreId() { $counterPath = $this->storePath . '_cnt.sdb'; if ( file_exists( $counterPath ) ) { return (int) file_get_contents( $counterPath ); } return 0; } /** * Get a store by its system id. "_id" * @param $id * @return array|mixed */ private function getStoreDocumentById( $id ) { $store = $this->storePath . 'data/' . $id . '.json'; if ( file_exists( $store ) ) { $data = json_decode( file_get_contents( $store ), true ); if ( $data !== false ) return $data; } return []; } /** * @param string $file * @return mixed */ private function getDocumentByPath ( $file ) { return @json_decode( @file_get_contents( $file ), true ); } /** * @param string $condition * @param mixed $fieldValue value of current field * @param mixed $value value to check * @throws ConditionNotAllowedException * @return bool */ private function verifyWhereConditions ( $condition, $fieldValue, $value ) { // Check the type of rule. if ( $condition === '=' ) { // Check equal. return ( $fieldValue == $value ); } else if ( $condition === '!=' ) { // Check not equal. return ( $fieldValue != $value ); } else if ( $condition === '>' ) { // Check greater than. return ( $fieldValue > $value ); } else if ( $condition === '>=' ) { // Check greater equal. return ( $fieldValue >= $value ); } else if ( $condition === '<' ) { // Check less than. return ( $fieldValue < $value ); } else if ( $condition === '<=' ) { // Check less equal. return ( $fieldValue <= $value ); } else if (strtolower($condition) === 'like'){ $value = str_replace('%', '(.)*', $value); $pattern = "/^".$value."$/i"; return (preg_match($pattern, $fieldValue) === 1); } throw new ConditionNotAllowedException('condition '.$condition.' is not allowed'); } /** * @return array * @throws IndexNotFoundException * @throws ConditionNotAllowedException * @throws EmptyFieldNameException * @throws InvalidDataException */ private function findStoreDocuments() { $found = []; // Start collecting and filtering data. $storeDataPath = $this->storePath . 'data/'; if( $handle = opendir($storeDataPath) ) { while ( false !== ($entry = readdir($handle)) ) { if ($entry != "." && $entry != "..") { $file = $storeDataPath . $entry; $data = $this->getDocumentByPath( $file ); $document = false; if ( ! empty( $data ) ) { // Filter data found. if ( empty( $this->conditions ) ) { // Append all data of this store. $document = $data; } else { // Append only passed data from this store. $storePassed = true; // Iterate each conditions. foreach ( $this->conditions as $condition ) { if ( $storePassed === true ) { // Check for valid data from data source. $validData = true; $fieldValue = ''; try { $fieldValue = $this->getNestedProperty( $condition[ 'fieldName' ], $data ); } catch( \Exception $e ) { $validData = false; $storePassed = false; } if( $validData === true ) { $storePassed = $this->verifyWhereConditions( $condition[ 'condition' ], $fieldValue, $condition[ 'value' ] ); } } } // Check if current store is updatable or not. if ( $storePassed === true ) { // Append data to the found array. $document = $data; } else { // Check if a or-where condition will allow this document. foreach ( $this->orConditions as $condition ) { // Check for valid data from data source. $validData = true; $fieldValue = ''; try { $fieldValue = $this->getNestedProperty( $condition[ 'fieldName' ], $data ); } catch( \Exception $e ) { $validData = false; $storePassed = false; } if( $validData === true ) { $storePassed = $this->verifyWhereConditions( $condition[ 'condition' ], $fieldValue, $condition[ 'value' ] ); if( $storePassed ) { // Append data to the found array. $document = $data; break; } } } } // Check if current store is updatable or not. if ( $storePassed === true ) { // Append data to the found array. $document = $data; } else if(count($this->orConditionsWithAnd) > 0) { // Check if a all conditions will allow this document. $allConditionMatched = true; foreach ( $this->orConditionsWithAnd as $condition ) { // Check for valid data from data source. $validData = true; $fieldValue = ''; try { $fieldValue = $this->getNestedProperty( $condition[ 'fieldName' ], $data ); } catch( \Exception $e ) { $validData = false; } if( $validData === true ) { $storePassed = $this->verifyWhereConditions( $condition[ 'condition' ], $fieldValue, $condition[ 'value' ] ); if($storePassed) continue; } // if data was invalid or store did not pass $allConditionMatched = false; break; } if( $allConditionMatched === true ) { // Append data to the found array. $document = $data; } } } // Completed condition checks. // IN clause. if( $document && !empty($this->in) ) { foreach ( $this->in as $inClause) { $validData = true; $fieldValue = ''; try { $fieldValue = $this->getNestedProperty( $inClause[ 'fieldName' ], $data ); } catch( \Exception $e ) { $validData = false; $document = false; break; } if( $validData === true ) { if( !in_array( $fieldValue, $inClause[ 'value' ] ) ) { $document = false; break; } } } } // notIn clause. if ( $document && !empty($this->notIn) ) { foreach ( $this->notIn as $notInClause) { $validData = true; $fieldValue = ''; try { $fieldValue = $this->getNestedProperty( $notInClause[ 'fieldName' ], $data ); } catch( \Exception $e ) { $validData = false; break; } if( $validData === true ) { if( in_array( $fieldValue, $notInClause[ 'value' ] ) ) { $document = false; break; } } } } // Check if there is any document appendable. if( $document ) { $found[] = $document; } } } } closedir( $handle ); } if ( count( $found ) > 0 ) { // Check do we need to sort the data. if ( $this->orderBy[ 'order' ] !== false ) { // Start sorting on all data. $found = $this->sortArray( $this->orderBy[ 'field' ], $found, $this->orderBy[ 'order' ] ); } // If there was text search then we would also sort the result by search ranking. if ( ! empty( $this->searchKeyword ) ) { $found = $this->performSearch( $found ); } // Skip data if ( $this->skip > 0 ) $found = array_slice( $found, $this->skip ); // Limit data. if ( $this->limit > 0 ) $found = array_slice( $found, 0, $this->limit ); } if(count($found) > 0){ if(count($this->fieldsToSelect) > 0){ $found = $this->applyFieldsToSelect($found); } if(count($this->fieldsToExclude) > 0){ $found = $this->applyFieldsToExclude($found); } } return $found; } /** * @param array $found * @return array */ private function applyFieldsToSelect($found){ if(!(count($found) > 0) || !(count($this->fieldsToSelect) > 0)){ return $found; } foreach ($found as $key => $item){ $newItem = []; $newItem['_id'] = $item['_id']; foreach ($this->fieldsToSelect as $fieldToSelect){ if(array_key_exists($fieldToSelect, $item)){ $newItem[$fieldToSelect] = $item[$fieldToSelect]; } } $found[$key] = $newItem; } return $found; } /** * @param array $found * @return array */ private function applyFieldsToExclude($found){ if(!(count($found) > 0) || !(count($this->fieldsToExclude) > 0)){ return $found; } foreach ($found as $key => $item){ foreach ($this->fieldsToExclude as $fieldToExclude){ if(array_key_exists($fieldToExclude, $item)){ unset($item[$fieldToExclude]); } } $found[$key] = $item; } return $found; } /** * Writes an object in a store. * @param $storeData * @return array * @throws IOException * @throws JsonException * @throws IdNotAllowedException */ private function writeInStore( $storeData ) { // Cast to array $storeData = (array) $storeData; // Check if it has _id key if ( isset( $storeData[ '_id' ] ) ) throw new IdNotAllowedException( 'The _id index is reserved by SleekDB, please delete the _id key and try again' ); $id = $this->getStoreId(); // Add the system ID with the store data array. $storeData[ '_id' ] = $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 $storePath = $this->storePath . 'data/' . $id . '.json'; if ( ! file_put_contents( $storePath, $storableJSON ) ) { throw new IOException( "Unable to write the object file! Please check if PHP has write permission." ); } return $storeData; } /** * Sort store objects. * @param $field * @param $data * @param string $order * @return array * @throws IndexNotFoundException * @throws EmptyFieldNameException * @throws InvalidDataException */ private function sortArray( $field, $data, $order = 'ASC' ) { $dryData = []; // Check if data is an array. if( is_array( $data ) ) { // Get value of the target field. foreach ( $data as $value ) { $dryData[] = $this->getNestedProperty( $field, $value ); } } // Descide the order direction. if ( strtolower( $order ) === 'asc' ) asort( $dryData ); else if ( strtolower( $order ) === 'desc' ) arsort( $dryData ); // Re arrange the array. $finalArray = []; foreach ( $dryData as $key => $value) { $finalArray[] = $data[ $key ]; } return $finalArray; } /** * Get nested properties of a store object. * @param string $fieldName * @param array $data * @return array|mixed * @throws EmptyFieldNameException * @throws IndexNotFoundException * @throws InvalidDataException */ private function getNestedProperty($fieldName, $data ) { if( !is_array( $data ) ) throw new InvalidDataException('data has to be an array'); if(empty( $fieldName )) throw new EmptyFieldNameException('fieldName is not allowed to be empty'); // Dive deep step by step. foreach(explode( '.', $fieldName ) as $i ) { // If the field do not exists then insert an empty string. if ( ! isset( $data[ $i ] ) ) { $data = ''; throw new IndexNotFoundException( '"'.$i.'" index was not found in the provided data array' ); } // The index is valid, collect the data. $data = $data[ $i ]; } return $data; } /** * Do a search in store objects. This is like a doing a full-text search. * @param array $data * @return array */ private function performSearch($data = [] ) { if ( empty( $data ) ) return $data; $nodesRank = []; // Looping on each store data. foreach ($data as $key => $value) { // Looping on each field name of search-able fields. foreach ($this->searchKeyword[ 'field' ] as $field) { try { $nodeValue = $this->getNestedProperty( $field, $value ); // The searchable field was found, do comparison against search keyword. similar_text( strtolower($nodeValue), strtolower($this->searchKeyword['keyword']), $perc ); if ( $perc > 50 ) { // Check if current store object already has a value, if so then add the new value. if ( isset( $nodesRank[ $key ] ) ) $nodesRank[ $key ] += $perc; else $nodesRank[ $key ] = $perc; } } catch ( \Exception $e ) { continue; } } } if ( empty( $nodesRank ) ) { // No matched store was found against the search keyword. return []; } // Sort nodes in descending order by the rank. arsort( $nodesRank ); // Map original nodes by the rank. $nodes = []; foreach ( $nodesRank as $key => $value ) { $nodes[] = $data[ $key ]; } return $nodes; } }