| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590 |
- <?php
- namespace SleekDB\Traits;
- use SleekDB\Exceptions\ConditionNotAllowedException;
- use SleekDB\Exceptions\IdNotAllowedException;
- use SleekDB\Exceptions\IOException;
- use SleekDB\Exceptions\InvalidConfigurationException;
- use SleekDB\Exceptions\EmptyStoreNameException;
- use SleekDB\Exceptions\IndexNotFoundException;
- use SleekDB\Exceptions\JsonException;
- use SleekDB\Exceptions\EmptyFieldNameException;
- use SleekDB\Exceptions\InvalidDataException;
- /**
- * Collections of method that helps to manage the data.
- * All methods in this trait should be private.
- *
- */
- trait HelperTrait {
- /**
- * @param array $conf
- * @throws IOException
- * @throws InvalidConfigurationException
- */
- private function init( $conf ) {
- // Check for valid configurations.
- if( empty( $conf ) OR !is_array( $conf ) ) throw new InvalidConfigurationException( 'Invalid configurations was found.' );
- // Check if the 'data_directory' was provided.
- if ( !isset( $conf[ 'data_directory' ] ) ) throw new InvalidConfigurationException( '"data_directory" was not provided in the configurations.' );
- // Check if data_directory is empty.
- if ( empty( $conf[ 'data_directory' ] ) ) throw new InvalidConfigurationException( '"data_directory" cant be empty in the configurations.' );
- // Prepare the data directory.
- $dataDir = trim( $conf[ 'data_directory' ] );
- // Handle directory path ending.
- if ( substr( $dataDir, -1 ) !== '/' ) $dataDir = $dataDir . '/';
- // Check if the data_directory exists.
- if ( !file_exists( $dataDir ) ) {
- // The directory was not found, create one.
- if ( !mkdir( $dataDir, 0777, true ) ) throw new IOException( 'Unable to create the data directory at ' . $dataDir );
- }
- // Check if PHP has write permission in that directory.
- if ( !is_writable( $dataDir ) ) throw new IOException( 'Data directory is not writable at "' . $dataDir . '." Please change data directory permission.' );
- // Finally check if the directory is readable by PHP.
- if ( !is_readable( $dataDir ) ) throw new IOException( 'Data directory is not readable at "' . $dataDir . '." Please change data directory permission.' );
- // Set the data directory.
- $this->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;
- }
-
- }
-
|