Свой фильтр

В разделе Обзор мы рассмотрели компонент bitrix:main.ui.filter, который отвечает за отображение и работу фильтра. Мы обсудили что для работы фильтрации необходимо чтобы были заполнены FIELDS и FILTER_ID параметры и рассмотрели примеры их заполнения. До этого момента мы обсуждали какие механизмы работаю вокруг фильтра, но не затрагивали работу с самим фильтром. Мы будем использовать некоторые возможности тулбара, поэтому если вы не читали эту статью - самое время.

Представим абстрактную задачу: у нас есть некоторая страница на которой нам нужно нарисовать фильтр и обработать применение этого фильтра. Из полей доступных к фильтрации у нас имеется полный набор:

  • Название (string)
  • Дата создания (data)
  • Пользователь (entity_selector) и комбинированный элемент Поиск, который ищет по нашему полнотекстовому полю.

Вся дальнейшая работа будет происходить над этой абстрактной задачей.

Архитектура фильтра

Сам по себе фильтр это исключительно обертка над так называемыми провайдерами данных, который общаясь с входящими параметрами может доставать указанные в провайдерах данных поля и возвращать их в подготовленном виде. Теперь мы знаем что у нас есть сам объект фильтра и провайдеры данных, которые выполняют роль поставщиков полей.

Объект фильтра

В современном подходе любой фильтр это объект/наследник класса \Bitrix\Main\Filter\Filter, который собирается из параметров:

  • Идентификатор фильтра (ID (string)
  • Главного провайдера (наследник \Bitrix\Main\Filter\DataProvider)
  • Опционального набора дополнительных провайдеров (набор провайдеров-наследников \Bitrix\Main\Filter\DataProvider)
  • Опционального набора дополнительных параметров

Получение фильтра

Существует два способа получить объект фильтра: явно создать объект и использовать фабрику.

Рассмотрим прямое (явное) создание объекта фильтра для сотрудников:

use \Bitrix\Main\Filter\Filter;
use \Bitrix\Main\Filter\UserDataProvider;
use \Bitrix\Main\Filter\UserUFDataProvider;

$myFilter = new Filter(
	"MY_FILTER_ID",
	new UserDataProvider($settings),
	[ new UserUFDataProvider($settings) ]
);

Сейчас работа дата провайдеров и содержимое $settings лежит вне нашего поля зрения, к ним мы вернемся позже.

В приведенном выше примере мы создали объект фильтра с основным провайдеров - пользовательской информацией и дополнительным провайдером - пользовательскими полями сотрудника.

Рассмотрим создание фильтра с использованием фабрики. Для создания фильтра у фабрики (\Bitrix\Main\Filter\Factory) существует статический метод createEntityFilter с сигнатурой:

\Bitrix\Main\Filter\Factory::createEntityFilter(
	$entityTypeName,
	array $settingsParams,
	array $additionalParams = null
);

Поэтому пример вызова/создания объекта фильтра (подобному, но такому же как в примере выше) выглядит следующим образом:

use \Bitrix\Main\Filter\Factory;
use \Bitrix\Main\UserTable;

$myFilter = Factory::createEntityFilter(
	UserTable::getUfId(),
	$settings
);

Подобно примеру выше $settings лежит вне нашего поля зрения, к ним мы вернемся позже.

Однако для того чтобы фабрика заработала, необходимо научить механизм фабрики создавать объекты нашего фильтра. Необходимо зарегистрировать свой обработчик события main::OnBuildFilterFactoryMethods и вернуть ассоциативный набор фильтров. Мы рассмотрим его на примере стандартного фильтра:

Регистрируем свой обработчик события:

$eventManager->registerEventHandler('main', 'OnBuildFilterFactoryMethods', 'main', '\Bitrix\Main\Filter\FactoryMain', 'onBuildFilterFactoryMethods');

Код класса-обработчика:

<?php
namespace Bitrix\Main\Filter;

use Bitrix\Main\Event;
use Bitrix\Main\EventResult;
use Bitrix\Main\UserTable;

class FactoryMain
{
	public static function onBuildFilterFactoryMethods(Event $event)
	{
		$result = new EventResult(
			EventResult::SUCCESS,
			[
				'callbacks' => [
					UserTable::getUfId() => function($entityTypeName, array $settingsParams, array $additionalParams = null) {

						if ($entityTypeName == UserTable::getUfId())
						{
							$settings = new UserSettings($settingsParams);
							$filterID = $settings->getID();

							return new Filter(
								$filterID,
								new UserDataProvider($settings),
								[
									new UserUFDataProvider($settings)
								],
								$additionalParams
							);

						}
					}
				]
			],
			'main'
		);

		return $result;
	}
}

Этот код из стандартной документации, поэтому после копирования необходимо изменить его под свои условия.

Несмотря на наличие возможности работать с \Bitrix\Main\Filter\Filter я рекомендую создавать наследника (пусть даже пустой), по некоторым причинам:

  1. Вам проще будет контролировать аргументы ваших функций, когда вы будете зависеть от фильтра.
  2. Вы сможете переопределять некоторые методы. Например, метод prepareFilterValue, т.е. стандартный фильтр плохо работает с диапазонами значений.

Работа с фильтром

Когда у нас есть объект фильтра мы можем выполнять необходимые нам действия. В коде предполагается что в $myFilter находится объект наследник нашего \Bitrix\Main\Filter\Filter.

Получение значения фильтра

Основное наше действие с фильтром: получение значений, который пользователь выбрал в своем фильтре. В общем виде это:

$myFilterFromRequest = $myFilter->getValue();

После этого в $myFilterFromRequest находится подготовленный массив к использованию в качестве фильтра DataManager-а.

Помимо REQUEST мы так же можем передать массив для обработки явно, например так:


$myRequestParameters = [
	'FIND'            => 'abc',
	'MY_FIELD_numsel' => 'exact',
	'MY_FIELD_from'   => '10',
	'MY_FIELD_to'     => '50',
];

$myFilterFromRequest = $myFilter->getValue( $myRequestParameters );

Получение доступных для фильтрации полей

За получение (или возврат) полей доступных для фильтрации отвечает метод getFields, который возвращает объекты полей из всех провайдеров данных фильтра. Однако нам более интересен не набор объектов, а набор подготовленных полей для передачи в компонент фильтра. Для этого существует метод getFieldArrays(array $fieldMask = []).

Пример получения полей:

$filterFields = $myFilter->getFieldArrays();

Дополнительные методы

Наряду с основными методами в фильтре существует ряд вспомогательных методов, которые могут быть полезны при использовании:

  • getID() - возвращает идентификатор фильтра
  • getDefaultFieldIDs() - возвращает поля отмеченные полями по-умолчанию

Провайдеры данных

Как мы уже обсуждали фильтр состоит из двух компонентов - обертки и провайдеров данных. С оберткой мы уже разобрались и самое время приступить к разбору провайдера.

Любой провайдер является наследником абстрактного класса \Bitrix\Main\Filter\DataProvider и обязан реализовывать следующие методы:

  • getSettings(): \Bitrix\Main\Filter\Settings - возвращает объект настроек фильтра
  • prepareFields(): array - возвращает набор описаний полей фильтра
  • prepareFieldData($fieldID): array|null - возвращает мета-описание поля или null если такого описания нет

Иногда удобнее наследоваться от класса Bitrix\Main\Filter\EntityDataProvider который предоставляет чуть более измененный DataProvider: автоматически подставляет названия полей вместо кодов, но требует реализации дополнительного метода getFieldName($fieldID): string.

Пример кода своего провайдера данных:

namespace Vendor\Module\Filter;

use \Bitrix\Main,
	\Bitrix\Main\Filter\Settings,
	\Bitrix\Main\Filter\EntityDataProvider,
	\Bitrix\Main\UI\Filter\DateType,
	\Bitrix\Main\Localization\Loc,
	\Bitrix\Main\Config\Option
	;

class MyDataProvider extends EntityDataProvider
{
	/** @var Settings|null */
	protected $settings = null;

	function __construct(Settings $settings)
	{
		$this->settings = $settings;
	}

	/**
	 * Get Settings
	 * @return Settings
	 */
	public function getSettings(): ?Settings
	{
		return $this->settings;
	}

	/**
	 * Prepare field list.
	 * @return Field[]
	 */
	public function prepareFields(): array
	{
		$result =  [
			'TITLE' => $this->createField(
				'TITLE',
				[
					'type'    => 'string',
					'partial' => true
				]
			),
			'TYPE' => $this->createField(
				'TYPE',
				[
					'type'    => 'list',
					'partial' => true
				]
			),
			'CREATED_AT' => $this->createField(
				'CREATED_AT',
				[
					'type'    => 'date'
				]
			),
			'AUTHOR_ID' => $this->createField(
				'AUTHOR_ID',
				[
					'type'    => 'entity_selector',
					'partial' => true
				]
			),
		];

		return $result;
	}

	/**
	 * Get specified entity field caption.
	 * @param string $fieldID Field ID.
	 * @return string
	 */
	protected function getFieldName($fieldID)
	{
		switch ($fieldID)
		{
			case "TITLE":
				return Loc::getMessage('VENDOR_MODULE_MYDATAPROVIDER_FILTER_FIELD_TITLE');
				break;

			case "TYPE":
				return Loc::getMessage('VENDOR_MODULE_MYDATAPROVIDER_FILTER_FIELD_TYPE');
				break;

			case "AUTHOR_ID":
				return Loc::getMessage('VENDOR_MODULE_MYDATAPROVIDER_FILTER_FIELD_AUTHOR_ID');
				break;

			case "CREATED_AT":
				return Loc::getMessage('VENDOR_MODULE_MYDATAPROVIDER_FILTER_FIELD_CREATED_AT');
				break;

			default: 
				return Loc::getMessage('VENDOR_MODULE_MYDATAPROVIDER_FILTER_FIELD_UNKNOWN');
		}
	}

	/**
	 * Prepare field parameter for specified field.
	 * @param array $filter Filter params.
	 * @param string $fieldId Field ID.
	 * @return void
	 */
	public function prepareListFilterParam(array &$filter, $fieldId)
	{
		if ( mb_substr($fieldId, -8) == DateType::getPostfix() )
		{
			$fieldId = mb_substr($fieldId, 0, -8);

			if ( array_key_exists($fieldId."_from", $filter) )
				$filter[">=".$fieldId] = $filter[$fieldId."_from"];
			if (array_key_exists($fieldId."_to", $filter))
				$filter["<=".$fieldId] = $filter[$fieldId."_to"];
		}
	}

	/**
	 * Prepare complete field data for specified field.
	 * @param string $fieldID Field ID.
	 * @return array|null
	 * @throws Main\NotSupportedException
	 */
	public function prepareFieldData($fieldID): ?array
	{
		if ( $fieldID == 'TITLE')
		{
			return [
				'params' => ['multiple' => 'N'],
			];
		}
		elseif ( $fieldID == 'TYPE')
		{
			$item = [
				"K1" => "Key one",
				"K2" => "Key two"
			];

			return [
				'params' => ['multiple' => 'Y'],
				'items'  => $item
			];
		}
		elseif ( $fieldID == 'CREATED_AT')
		{
			return [
				'params' => ['multiple' => 'N'],
			];
		}
		elseif ( $fieldID == 'AUTHOR_ID')
		{
			return [
				'params' => [
					'multiple'      => 'Y',
					'dialogOptions' => [
						'height'       => 200,
						'context'      => $this->getSettings()->getId(),
						'entities'     => [
							[
								'id'      => 'user',
								'options' => [
									'inviteEmployeeLink' => false,
									'intranetUsersOnly'  => true,
								]
							],
						],
						'showAvatars'  => true,
						'dropdownMode' => false,
					],
				],
			];
		}

		return null;
	}
}

Примеры

Пример вывода фильтра:

use \Bitrix\Main\Filter\Filter;
use \Bitrix\UI\Toolbar\Facade\Toolbar;
use \Vendor\Module\Filter\MyDataProvider;
use \Bitrix\Main\Filter\Settings;

$myFilterId = "CUSTOM_PAGE_FILTER";

$myFilter = new Filter(
	$myFilterId,
	new MyDataProvider(
		new Settings([
			'ID' => $myFilterId
		])
	),
	[],
	[]
);

Toolbar::addFilter([
	'FILTER_ID'          => $myFilter->getID(),
	'FILTER'             => $myFilter->getFieldArrays(),
	'FILTER_PRESETS'     => [],
	'ENABLE_LIVE_SEARCH' => true,
	'DISABLE_SEARCH'     => false,
	'ENABLE_LABEL'       => true,
]);

Пример работе с фильтром на backend:

use \Bitrix\Main\Filter\Filter;
use \Vendor\Module\Filter\MyDataProvider;
use \Bitrix\Main\Filter\Settings;

$myFilter = new Filter(
	$myFilterId,
	new MyDataProvider(
		new Settings([
			'ID' => $myFilterId
		])
	),
	[],
	[]
);

$someResult = SomeTable::getList([
	'select' => ['*'],
	'filter' => $myFilter->getValue()
]);