590 lines
22 KiB
PHP
590 lines
22 KiB
PHP
|
<?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;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|