Введение
Статья написана после того, как в реальном проекте появилась необходимость использовать сложные фильтры с большим набором условий и возможность простого масштабирования фильтрации. Было принято решение использовать ядро 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, ]; } }