Введение
Примечание: Код написанный для конкретного проекта и этот текст несет исключительно информационную ценность, возможно кому-то пригодится, как вариант реализации в вашем проекте.
Статья написана после того, как в реальном проекте появилась необходимость использовать сложные фильтры с большим набором условий и возможность простого масштабирования фильтрации. Было принято решение использовать ядро D7 и класс запросов Query для построения фильтра.
Какая задача решалась
Была база новостей с большим количеством различных параметром, например, таких как языковая версия новости, тип новости(Анонс, новость, мероприятие, видео и другие), отображать на определенном сайте или нет и множество других условий. Новостей было более 100 тыс., так что и производительность при выполнении запросов нужна была оптимальная.
Процесс написания фильтра
Еще одним из важных моментов было то, что данный фильтр необходимо было использоваться в разных компонентах, которые выводят новости для того, чтобы в рамкам одного сайта на главной странице и странице со списком новостей были одни и те же новости. Чтобы не было, так сказать, рассинхрона между страницами, вынесли это в модуль.Первое, что необходимо сделать при написании нового класса, это подключить модули, с которыми будем работать. В нашем случае достаточно было подключить модуль iblock и forumedia.common (собственная разработка для более удобного использования стандартного API).Заранее необходимо продумать все возможные варианты фильтрации и учесть их при построении логики. А так все поля и свойства, которые вам необходимы.
Обрабатываем входные параметры и значения по умолчанию
Первым делом решил написать небольшой обработчик для входящий параметров и параметров по умолчанию, которые не зависят от контента, который фильтруется. За это отвечает функция initParams. В ней мы сразу узнаем админ ли текущий пользователь или нет, а также активные новости будем достать или любые (это необходимо, так как в проекте для админов свои кнопки для редактирования новостей, а создатели новости видят в добавок только свои новости).Далее мы создали "справочники" возможных значений для свойств Языковая версия и Тип ресурса, на котором новость отображается и справочник Типов новости. Так же обработали входящие значения для более удобного использования в дальнейшем.В функции initLanguageList использовали ORM Bitrix с использованием кэша на 10 дней, так как нет смысла каждый раз получать список языков, он меняется очень редко, а может и никогда:)Обратите внимание на обработку $params['SECTION'].Добавлена проверка, что мы передали ID раздела или символьный код, мы не разделяли название параметра для указания раздела на SECTION_ID и SECTION_CODE во имя своего удобства.Думаю, суть понятна, что мы хотели сделать. Переходим к не менее важной части - построение самого фильтра. Функция getFilter().
Строим фильтр
Тут все просто, думаю особых объяснений не требуется, если что-то не понятно, можете оставить комментарий, подскажу и помогу.Хотелось бы отметить, что у нас реализована возможность поиска И по тегам, и по разделу, что вы можеет увидеть в коде.
return \Bitrix\Main\Entity\Query::filter() ->logic('or') ->where($this->filter) ->where($this->filterTags);
Исходный код класса Filter
class Filter{
const IBLOCK_ID = 400;
const PROPERTY_ELEMENT_TYPE_ID = 2454;
const LANGUAGE_LIST_CACHE_TIME = 864000;
const SET_DEFAULT_LANGUAGE = 'ru';
private $filter, $filterTags, $user, $data, $list;
public function __construct(array $params){
Loader::includeModule('iblock');
Loader::includeModule('forumedia.common');
self::initParams($params);
}
public function getFilter():Query\Filter\ConditionTree{
$this->filter = Query::filter();
if($this->data->isActive){
$this->filter->where('ACTIVE', '=', $this->data->isActive);
}
if(!is_null($this->data->activeFrom)){
$this->filter->where('ACTIVE_FROM', '>=', $this->data->activeFrom);
}
if(!is_null($this->data->activeTo)){
$this->filter->where('ACTIVE_FROM', '<=', $this->data->activeTo);
}
$this->filter->where('ELEMENT_TYPE.VALUE', '=', $this->data->elementType);
$this->filter->whereNotIn('UNSET_RESOURCES.VALUE', $this->data->resources->id);
if($this->data->language->code === 'ru'){
$this->filter->where('LANGUAGE_VERSION.VALUE', '=',
Query::filter()
->logic('or')
->whereNull('LANGUAGE_VERSION.VALUE')
->where('LANGUAGE_VERSION.VALUE', $this->data->language->id)
);
}else{
$this->filter->where('LANGUAGE_VERSION.VALUE', '=', $this->data->language->id);
}
// Фильтр только по тегам
if($this->data->tags && empty($this->data->section)){
$this->filter = Query::filter()->where('IBLOCK_ID', '=', self::IBLOCK_ID);
$this->filter->whereIn('ID', \forumedia\common\iblock::searchByTags($this->data->tags, self::IBLOCK_ID));
return $this->filter;
}
// Фильтра по тегу или по разделу
if($this->data->tags && !empty($this->data->section)){
$this->filterTags = $this->filter->getConditions();
$this->filterTags = Query::filter()->where('IBLOCK_ID', '=', self::IBLOCK_ID);
$this->filterTags->whereIn('ID', \forumedia\common\iblock::searchByTags($this->data->tags, self::IBLOCK_ID));
$this->filter->where($this->data->sectionType, '=', $this->data->section);
return Query::filter()
->logic('or')
->where($this->filter)
->where($this->filterTags);
}
// Фильтр только по разделу
$this->filter->where($this->data->sectionType, '=', $this->data->section);
return $this->filter;
}
protected function initParams(array $params = []){
$this->user->isAdmin = $GLOBALS['USER']->IsAdmin();
$this->user->isGlobalModerator = \forumedia\common\subdomain::isGlobalModerator();
$this->user->isResourcesModerator = \forumedia\common\subdomain::isModerator();
$this->data->isActive = (!$this->user->isGlobalModerator || ($params['ONLY_ACTIVE'] === 'Y')) ?: null;
$this->data->activeFrom = $params['ACTIVE_FROM'] ? \Bitrix\Main\Type\DateTime::createFromTimestamp($params['ACTIVE_FROM']) : null;
$this->data->activeTo = $params['ACTIVE_TO'] ? \Bitrix\Main\Type\DateTime::createFromTimestamp($params['ACTIVE_TO']) : null;
self::initElementTypeList();
$this->data->elementType = $this->list->elementType->{$params['ELEMENT_TYPE']} ?: $this->list->elementType->news; //default is element type - news
if($params['SECTION']){
if(!is_numeric($params['SECTION'])){
$this->data->section = $params['SECTION'];
$this->data->sectionType = 'IBLOCK_SECTION.CODE';
}else{
$this->data->section = intval($params['SECTION']);
$this->data->sectionType = 'IBLOCK_SECTION.ID';
}
}
self::initLanguageList();
$langCode = ($params['LANGUAGE'] ?: \LANGUAGE_ID) ?: self::SET_DEFAULT_LANGUAGE;
$this->data->language->id = $this->list->languages->{$langCode};
$this->data->language->code = $langCode;
$this->data->resources->id = self::initResources($params['RESOURCES']);
if($params['FILTER_TAGS'] && is_array($params['FILTER_TAGS']))
$this->data->tags = $params['FILTER_TAGS'];
}
protected function initElementTypeList(){
if(empty($this->list->elementType)){
$collection = \Bitrix\Iblock\PropertyEnumerationTable::getList(array(
'filter' => array('PROPERTY_ID' => self::PROPERTY_ELEMENT_TYPE_ID)
))->fetchCollection();
foreach ($collection as $key => $enum) {
$this->list->elementType->{$enum->getXmlId()} = $enum->getId();
}
}
}
protected function initLanguageList(){
if(empty($this->list->languages)){
$collection = \Bitrix\Iblock\Elements\ElementLanguageTable::getList([
'select' => ['ID', 'CODE'],
'filter' => ['=ACTIVE' => true],
'cache' => ['ttl' => self::LANGUAGE_LIST_CACHE_TIME] //10 дней
])->fetchCollection();
foreach ($collection as $language) {
$this->list->languages->{$language->getCode()} = $language->getId();
}
}
}
protected function initResources(string $params = null):string{
return (\forumedia\common\subdomain::getCurrentResources($params))['ID'];
}
public static function getDefaultSelect():array{
return [
'ID',
'NAME',
'CODE',
'PREVIEW_PICTURE',
'DETAIL_PICTURE',
'DATE_CREATE',
'ACTIVE_FROM',
'ACTIVE',
'SECTION_CODE' => 'IBLOCK_SECTION.CODE',
'TYPE' => 'ELEMENT_TYPE.VALUE',
'DATE_ANONCE_START_' => 'DATE_ANONCE_START.VALUE',
'UNSET_RESOURCES_' => 'UNSET_RESOURCES.VALUE',
'LANGUAGE_VERSION_' => 'LANGUAGE_VERSION.VALUE',
'IBLOCK_SECTION_ID_' => 'IBLOCK_SECTION.ID',
'IBLOCK_ID',
'PREVIEW_TEXT',
'DETAIL_TEXT',
'TAGS'
];
}
public static function getDefaultCache():array{
return [
'ttl' => 600,
'cache_joins' => true,
];
}
}