%PDF- %PDF-
| Direktori : /home/bitrix/www/bitrix/modules/sale/lib/location/search/ |
| Current File : //home/bitrix/www/bitrix/modules/sale/lib/location/search/finder.php |
<?php
/**
* Bitrix Framework
* @package Bitrix\Sale\Location
* @subpackage sale
* @copyright 2001-2014 Bitrix
*/
namespace Bitrix\Sale\Location\Search;
use Bitrix\Main;
use Bitrix\Main\DB;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Config\Option;
use Bitrix\Sale\Location;
use Bitrix\Sale\Location\Util\Assert;
Loc::loadMessages(__FILE__);
class Finder
{
const SALE_LOCATION_INDEXED_TYPES_OPT = 'sale.location.indexed_types';
const SALE_LOCATION_INDEXED_LANGUAGES_OPT = 'sale.location.indexed_langs';
const SALE_LOCATION_INDEX_VALID_OPT = 'sale.location.index_valid';
protected static $allowedOperations = array(
'=' => true
);
public static function checkIndexValid()
{
return Option::get('sale', self::SALE_LOCATION_INDEX_VALID_OPT, '', '') == 'Y';
}
public static function setIndexValid()
{
Option::set('sale', self::SALE_LOCATION_INDEX_VALID_OPT, 'Y', '');
}
public static function setIndexInvalid()
{
Option::set('sale', self::SALE_LOCATION_INDEX_VALID_OPT, 'N', '');
}
public static function getIndexedTypes()
{
$types = Option::get('sale', self::SALE_LOCATION_INDEXED_TYPES_OPT, '', '');
$typesFromDb = static::getTypesFromDb();
if(!strlen($types)) // means "all"
return array_keys($typesFromDb);
$types = explode(':', $types);
$result = array();
if(is_array($types))
{
foreach($types as $type)
{
$type = intval($type);
if(isset($typesFromDb[$type]))
$result[] = $type;
}
}
return array_unique($result);
}
public static function setIndexedTypes($types = array())
{
$result = array();
if(is_array($types) && !empty($types))
{
$typesFromDb = static::getTypesFromDb();
foreach($types as $type)
{
$type = intval($type);
if(isset($typesFromDb[$type]))
$result[] = $type;
}
$result = array_unique($result);
}
Option::set('sale', self::SALE_LOCATION_INDEXED_TYPES_OPT, implode(':', $result), '');
}
public static function getIndexedLanguages()
{
$langs = Option::get('sale', self::SALE_LOCATION_INDEXED_LANGUAGES_OPT, '', '');
$langsFromDb = static::getLangsFromDb();
if(!strlen($langs))
return array_keys($langsFromDb);
$result = array();
$langs = explode(':', $langs);
if(is_array($langs))
{
foreach($langs as $lang)
{
if(isset($langsFromDb[$lang]))
$result[] = $lang;
}
}
return array_unique($result);
}
public static function setIndexedLanguages($langs = array())
{
if(is_array($langs) && !empty($langs))
$langs = array_unique($langs);
else
$langs = array();
$result = array();
if(is_array($langs) && !empty($langs))
{
$langsFromDb = static::getLangsFromDb();
foreach($langs as $lang)
{
if(isset($langsFromDb[$lang]))
$result[] = $lang;
}
$result = array_unique($result);
}
Option::set('sale', self::SALE_LOCATION_INDEXED_LANGUAGES_OPT, implode(':', $result), '');
}
protected static function getLangsFromDb()
{
$langsFromDb = array();
$res = \Bitrix\Main\Localization\LanguageTable::getList(array('select' => array('ID')));
while($item = $res->fetch())
$langsFromDb[$item['ID']] = true;
return $langsFromDb;
}
protected static function getTypesFromDb()
{
$typesFromDb = array();
$res = Location\TypeTable::getList(array('select' => array('ID')));
while($item = $res->fetch())
$typesFromDb[intval($item['ID'])] = true;
return $typesFromDb;
}
/**
*
* $parameters is an ORM`s getList compatible array of parameters
*
*
*/
public static function find($parameters, $behaviour = array('FALLBACK_TO_NOINDEX_ON_NOTFOUND' => true, 'USE_INDEX' => true, 'USE_ORM' => true))
{
/////////////////////////////////
// parameter check and process
Assert::expectArray($parameters, '$parameters');
if(!is_array($behaviour))
$behaviour = array();
if(!isset($behaviour['FALLBACK_TO_NOINDEX_ON_NOTFOUND']))
$behaviour['FALLBACK_TO_NOINDEX_ON_NOTFOUND'] = true;
if(!isset($behaviour['USE_INDEX']))
$behaviour['USE_INDEX'] = true;
if(!isset($behaviour['USE_ORM']))
$behaviour['USE_ORM'] = true;
if(!isset($parameters['select']))
$parameters['select'] = array('ID');
Assert::expectArray($parameters['select'], '$parameters[select]');
if(isset($parameters['filter']))
{
Assert::expectArray($parameters['filter'], '$parameters[filter]');
// spikes, refactor later
if(isset($parameters['filter']['PHRASE']) || isset($parameters['filter']['=PHRASE']))
{
$key = isset($parameters['filter']['PHRASE']) ? 'PHRASE' : '=PHRASE';
$parameters['filter'][$key] = Assert::expectStringNotNull($parameters['filter'][$key], '$parameters[filter]['.$key.']');
$parameters['filter'][$key] = str_replace('%', '', $parameters['filter'][$key]); // cannot pass '%' to like
}
if(isset($parameters['filter']['SITE_ID']) || isset($parameters['filter']['=SITE_ID']))
{
$key = isset($parameters['filter']['SITE_ID']) ? 'SITE_ID' : '=SITE_ID';
$parameters['filter'][$key] = Assert::expectStringNotNull($parameters['filter'][$key], '$parameters[filter]['.$key.']'); // stronger here
if(!Location\SiteLocationTable::checkLinkUsageAny($parameters['filter'][$key]))
unset($parameters['filter'][$key]);
}
}
if(isset($parameters['limit']))
$parameters['limit'] = Assert::expectIntegerNonNegative($parameters['limit'], '$parameters[limit]');
if(isset($parameters['offset']))
$parameters['offset'] = Assert::expectIntegerNonNegative($parameters['offset'], '$parameters[offset]');
/////////////////////////////////
if(
(isset($parameters['filter']['PHRASE']) || isset($parameters['filter']['SITE_ID']) || isset($parameters['filter']['=PHRASE']) || isset($parameters['filter']['=SITE_ID']))
||
$behaviour['USE_ORM'] === false
)
{
if(static::checkIndexValid() && $behaviour['USE_INDEX'])
{
$result = static::findUsingIndex($parameters);
if(!$behaviour['FALLBACK_TO_NOINDEX_ON_NOTFOUND'])
{
return $result;
}
else
{
$temporalBuffer = array();
while($item = $result->fetch())
{
$temporalBuffer[] = $item;
}
if(empty($temporalBuffer))
{
return static::findNoIndex($parameters);
}
else
{
return new DB\ArrayResult($temporalBuffer);
}
}
}
else
{
return static::findNoIndex($parameters);
}
}
else
{
return Location\LocationTable::getList($parameters);
}
}
protected static function parseFilter($filter)
{
$parsed = array();
if(is_array($filter))
{
foreach($filter as $field => $value)
{
$found = array();
preg_match("#^(=?)(.+)#", $field, $found);
if(strlen($found[1]))
$op = $found[1];
else
$op = '=';
if(!isset(static::$allowedOperations[$op]))
throw new Main\ArgumentException('Unknown modifier in the filter');
$fieldParsed = $found[2];
$parsed[$fieldParsed] = array(
'OP' => strlen($op) ? $op : '=',
'VALUE' => $value
);
}
}
return $parsed;
}
protected static function findUsingIndex($parameters)
{
$query = array();
$dbConnection = Main\HttpApplication::getConnection();
$dbHelper = Main\HttpApplication::getConnection()->getSqlHelper();
$filter = static::parseFilter($parameters['filter']);
$filterByPhrase = isset($filter['PHRASE']) && strlen($filter['PHRASE']['VALUE']);
if($filterByPhrase) // filter by phrase
{
$bounds = WordTable::getBoundsForPhrase($filter['PHRASE']['VALUE']);
$firstBound = array_shift($bounds);
$k = 0;
foreach($bounds as $bound)
{
$query['JOIN'][] = " inner join ".ChainTable::getTableName()." A".$k." on A.LOCATION_ID = A".$k.".LOCATION_ID and (
".($bound['INF'] == $bound['SUP']
? " A".$k.".POSITION = '".$bound['INF']."'"
: " A".$k.".POSITION >= '".$bound['INF']."' and A".$k.".POSITION <= '".$bound['SUP']."'"
)."
)";
$k++;
}
$query['WHERE'][] = (
$firstBound['INF'] == $firstBound['SUP']
? " A.POSITION = '".$firstBound['INF']."'"
: " A.POSITION >= '".$firstBound['INF']."' and A.POSITION <= '".$firstBound['SUP']."'"
);
$mainTableJoinCondition = 'A.LOCATION_ID';
}
else
{
$mainTableJoinCondition = 'L.ID';
}
// site link search
if(strlen($filter['SITE_ID']['VALUE']) && SiteLinkTable::checkTableExists())
{
$query['JOIN'][] = "inner join ".SiteLinkTable::getTableName()." SL on SL.LOCATION_ID = ".$mainTableJoinCondition." and SL.SITE_ID = '".$dbHelper->forSql($filter['SITE_ID']['VALUE'])."'";
}
// process filter and select statements
// at least, we support here basic field selection and filtration + NAME.NAME and NAME.LANGUAGE_ID
$map = Location\LocationTable::getMap();
$nameRequired = false;
$locationRequred = false;
if(is_array($parameters['select']))
{
foreach($parameters['select'] as $alias => $field)
{
if($field == 'NAME.NAME' || $field == 'NAME.LANGUAGE_ID')
{
$nameRequired = true;
continue;
}
if(
!isset($map[$field]) ||
!in_array($map[$field]['data_type'], array('integer', 'string', 'float', 'boolean')) ||
isset($map[$field]['expression'])
)
{
unset($parameters['select'][$alias]);
}
$locationRequred = true;
}
}
foreach($filter as $field => $params)
{
if($field == 'NAME.NAME' || $field == 'NAME.LANGUAGE_ID')
{
$nameRequired = true;
continue;
}
if(
!isset($map[$field]) ||
!in_array($map[$field]['data_type'], array('integer', 'string', 'float', 'boolean')) ||
isset($map[$field]['expression'])
)
{
unset($filter[$field]);
}
$locationRequred = true;
}
// data join, only if extended select specified
if($locationRequred && $filterByPhrase)
$query['JOIN'][] = "inner join ".Location\LocationTable::getTableName()." L on A.LOCATION_ID = L.ID";
if($nameRequired)
$query['JOIN'][] = "inner join ".Location\Name\LocationTable::getTableName()." NAME on NAME.LOCATION_ID = ".$mainTableJoinCondition; // and N.LANGUAGE_ID = 'ru'
// making select
if(is_array($parameters['select']))
{
$select = array();
foreach($parameters['select'] as $alias => $field)
{
if($field != 'NAME.NAME' && $field != 'NAME.LANGUAGE_ID')
$field = 'L.'.$dbHelper->forSql($field);
if((string) $alias === (string) intval($alias))
$select[] = $field;
else
$select[] = $field.' as '.$dbHelper->forSql($alias);
}
$sqlSelect = implode(', ', $select);
}
else
$sqlSelect = $mainTableJoinCondition.' as ID';
// making filter
foreach($filter as $field => $params)
{
if($field != 'NAME.NAME' && $field != 'NAME.LANGUAGE_ID')
$field = 'L.'.$dbHelper->forSql($field);
$values = $params['VALUE'];
if(!is_array($values))
$values = array($values);
foreach($values as $value)
$query['WHERE'][] = $field.' '.$params['OP']." '".$dbHelper->forSql($value)."'";
}
if($filterByPhrase)
{
$sql = "
select ".($dbConnection->getType() != 'mysql' ? '' : 'distinct')/*fix this in more clever way later*/."
".$sqlSelect.(\Bitrix\Sale\Location\DB\Helper::needSelectFieldsInOrderByWhenDistinct() ? ', A.RELEVANCY' : '')."
from ".ChainTable::getTableName()." A
".implode(' ', $query['JOIN'])."
".(count($query['WHERE']) ? 'where ' : '').implode(' and ', $query['WHERE'])."
order by A.RELEVANCY asc
";
}
else
{
$sql = "
select
".$sqlSelect."
from ".Location\LocationTable::getTableName()." L
".implode(' ', $query['JOIN'])."
".(count($query['WHERE']) ? 'where ' : '').implode(' and ', $query['WHERE'])."
";
}
$offset = intval($parameters['offset']);
$limit = intval($parameters['limit']);
if($limit)
$sql = $dbHelper->getTopSql($sql, $limit, $offset);
$res = $dbConnection->query($sql);
return $res;
}
/**
*
*
* @param
*
* @return
*/
protected static function findNoIndex($parameters)
{
$dbConnection = Main\HttpApplication::getConnection();
$dbHelper = $dbConnection->getSqlHelper();
// tables
$locationTable = Location\LocationTable::getTableName();
$locationNameTable = Location\Name\LocationTable::getTableName();
$locationGroupTable = Location\GroupLocationTable::getTableName();
$locationSiteTable = Location\SiteLocationTable::getTableName();
$locationTypeTable = Location\TypeTable::getTableName();
//////////////////////////////////
// sql parameters prepare
//////////////////////////////////
$filter = static::parseFilter($parameters['filter']);
if(strlen($filter['SITE_ID']['VALUE']))
{
$filterSite = $dbHelper->forSql(substr($filter['SITE_ID']['VALUE'], 0, 2));
$hasLocLinks = Location\SiteLocationTable::checkLinkUsage($filterSite, Location\SiteLocationTable::DB_LOCATION_FLAG);
$hasGrpLinks = Location\SiteLocationTable::checkLinkUsage($filterSite, Location\SiteLocationTable::DB_GROUP_FLAG);
$doFilterBySite = true;
}
if(strlen($filter['PHRASE']['VALUE']))
{
$doFilterByName = true;
$filterName = ToUpper($dbHelper->forSql($filter['PHRASE']['VALUE']));
}
if(intval($filter['ID']['VALUE']))
{
$doFilterById = true;
$filterId = intval($filter['ID']['VALUE']);
}
if(intval($filter['CODE']['VALUE']))
{
$doFilterByCode = true;
$filterCode = $dbHelper->forSql($filter['CODE']['VALUE']);
}
$doFilterByLang = true;
if(strlen($filter['NAME.LANGUAGE_ID']['VALUE']))
{
$filterLang = $dbHelper->forSql(substr($filter['NAME.LANGUAGE_ID']['VALUE'], 0, 2));
}
else
$filterLang = LANGUAGE_ID;
if(isset($filter['PARENT_ID']) && intval($filter['PARENT_ID']['VALUE']) >= 0)
{
$doFilterByParent = true;
$filterParentId = intval($filter['PARENT_ID']['VALUE']);
}
if(intval($filter['TYPE_ID']['VALUE']))
{
$doFilterByType = true;
$filterTypeId = intval($filter['TYPE_ID']['VALUE']);
}
// filter select fields
if(!is_array($parameters['select']))
$parameters['select'] = array();
$map = Location\LocationTable::getMap();
$nameAlias = false;
foreach($parameters['select'] as $alias => $field)
{
if($field == 'CHILD_CNT')
$doCountChildren = true;
if($field == 'NAME.NAME')
$nameAlias = $alias;
if(/*in_array($field, array('ID', 'CODE', 'SORT', 'LEFT_MARGIN', 'RIGHT_MARGIN')) || */
!isset($map[$field]) ||
!in_array($map[$field]['data_type'], array('integer', 'string', 'float', 'boolean')) ||
isset($map[$field]['expression'])
)
{
unset($parameters['select'][$alias]);
}
}
//////////////////////////////////
// sql query build
//////////////////////////////////
// mandatory fields to be selected anyway
// alias => field
$fields = array(
'L.ID' => 'L.ID',
'L.CODE' => 'L.CODE',
'L.SORT' => 'L.SORT',
'LT_SORT' => 'LT.DISPLAY_SORT'
);
if($nameAlias === false || !preg_match('#^[a-zA-Z0-9]+$#', $nameAlias))
{
$fields['NAME'] = 'LN.NAME';
}
else
{
$fields[$nameAlias] = 'LN.NAME';
}
$fields = array_merge($fields, array(
'L.LEFT_MARGIN' => 'L.LEFT_MARGIN',
'L.RIGHT_MARGIN' => 'L.RIGHT_MARGIN'
));
$groupFields = $fields;
// additional fields to select
foreach($parameters['select'] as $alias => $fld)
{
$lFld = 'L.'.$fld;
// check if field is already selected
if((string) $alias === (string) intval($alias))
{
// already selected
if(in_array($lFld, $fields))
continue;
$fields[$lFld] = $lFld;
//$groupFields[$lFld] = $lFld;
}
else // alias is not a number
{
if(isset($fields[$alias]))
continue;
$fields[$alias] = $lFld;
//$groupFields[$alias] = $lFld;
}
$groupFields[$lFld] = $lFld;
}
if($doCountChildren)
$fields['CHILD_CNT'] = 'COUNT(LC.ID)';
// make select sql
$selectSql = array();
foreach($fields as $alias => $fld)
{
if($fld == $alias)
$selectSql[] = $fld;
else
$selectSql[] = $fld.' as '.$alias;
}
$selectSql = implode(', ', $selectSql);
//$groupSql = implode(', ', array_keys($groupFields));
$groupSql = implode(', ', $groupFields);
$mainSql = "select {$selectSql}
from {$locationTable} L
inner join {$locationNameTable} LN on L.ID = LN.LOCATION_ID
inner join {$locationTypeTable} LT on L.TYPE_ID = LT.ID ".
($doCountChildren ? "
left join {$locationTable} LC on L.ID = LC.PARENT_ID
" : "")."
%SITE_FILTER_CONDITION%
where
%MAIN_FILTER_CONDITION%
%GROUP_BY%
";
$where = array();
if($doFilterByLang)
$where[] = "LN.LANGUAGE_ID = '".$filterLang."'";
if($doFilterByParent)
$where[] = "L.PARENT_ID = '".$filterParentId."'";
if($doFilterById)
$where[] = "L.ID = '".$filterId."'";
if($doFilterByCode)
$where[] = "L.CODE = '".$filterCode."'";
if($doFilterByType)
$where[] = "L.TYPE_ID = '".$filterTypeId."'";
if($doFilterByName)
$where[] = "LN.NAME_UPPER like '".$filterName."%'";
$mainSql = str_replace('%MAIN_FILTER_CONDITION%', implode(' and ', $where), $mainSql);
$needDistinct = false;
$unionized = false;
$artificialNav = false;
if(!$doFilterBySite)
{
$sql = str_replace('%SITE_FILTER_CONDITION%', '', $mainSql);
}
else
{
$sql = array();
if($hasLocLinks)
{
$sql[] = str_replace('%SITE_FILTER_CONDITION%', "
inner join {$locationTable} L2 on L2.LEFT_MARGIN <= L.LEFT_MARGIN and L2.RIGHT_MARGIN >= L.RIGHT_MARGIN
inner join {$locationSiteTable} LS2 on L2.ID = LS2.LOCATION_ID and LS2.LOCATION_TYPE = 'L' and LS2.SITE_ID = '{$filterSite}'
", $mainSql);
}
if($hasGrpLinks)
{
$sql[] = str_replace('%SITE_FILTER_CONDITION%', "
inner join {$locationTable} L2 on L2.LEFT_MARGIN <= L.LEFT_MARGIN and L2.RIGHT_MARGIN >= L.RIGHT_MARGIN
inner join {$locationGroupTable} LG on LG.LOCATION_ID = L2.ID
inner join {$locationSiteTable} LS2 on LG.LOCATION_GROUP_ID = LS2.LOCATION_ID and LS2.LOCATION_TYPE = 'G' and LS2.SITE_ID = '{$filterSite}'
", $mainSql);
$useDistinct = true;
}
$cnt = count($sql);
if($cnt == 1)
{
$needDistinct = true;
}
else
{
// UNION removes duplicates, so distinct is required only when no union here
$unionized = true;
}
$sql = ($cnt > 1 ? '(' : '').implode(') union (', $sql).($cnt > 1 ? ')' : '');
}
// set groupping if needed
$sql = str_replace('%GROUP_BY%', $needDistinct || $doCountChildren ? "group by {$groupSql}" : '', $sql);
if(!is_array($parameters['order']))
{
$sql .= " order by 3, 4 asc, 5";
}
else
{
// currenly spike
if(isset($parameters['order']['NAME.NAME']))
$sql .= " order by 5 ".($parameters['order']['NAME.NAME'] == 'asc' ? 'asc' : 'desc');
}
$offset = intval($parameters['offset']);
$limit = intval($parameters['limit']);
if($limit)
{
if($dbConnection->getType() == 'mssql')
{
// due to huge amount of limitations of windowed functions in transact, using artificial nav here
// (does not support UNION and integer indices in ORDER BY)
$artificialNav = true;
}
else
{
$sql = $dbHelper->getTopSql($sql, $limit, $offset);
}
}
$res = $dbConnection->query($sql);
if($artificialNav)
{
$result = array();
$i = -1;
while($item = $res->fetch())
{
$i++;
if($i < $offset)
continue;
if($i >= $offset + $limit)
break;
$result[] = $item;
}
return new DB\ArrayResult($result);
}
else
{
return $res;
}
}
}