Пользовательское свойство инфоблока, для связи с ORM таблицами
Кастомное свойство инфоблока, для связи с ORM таблицами
Сколько раз вам доводилось создавать инфоблоки со свойствами в которые нужно вводить какие либо числовые (или буквенные) значения? Это могли быть привязки к группам пользователя, или службам доставки, оплаты. А как насчет привязки к местоположению с вводом в элемент его CODE?!. Всегда хотелось иметь возможность "нормального" интерфейса для этих привязок.
Так появился на свет данный функционал. Как он работает?
В Битриксе имеется возможность создавать пользовательские свойства для ИБ. Пользовательские свойства модуля информационных блоков позволяют изменять представление (формы ввода и т.п.) стандартных свойств расширяя их возможности. Фактически такое свойство представляет собой набор обработчиков событий вызываемых при построении административного интерфейса, публичной части сайта или API функций.
При первом обращении к методам пользовательских свойств вызываются обработчики события OnIBlockPropertyBuildList. Строится список свойств и при необходимости вызываются их методы.
Примеры конкретной реализации свойств можно посмотреть в файлах модуля информационных блоков classes/general/prop_*.php .
Подробнее об API пользовательских свойств можно почитать в документации
Рассмотрим задачу на основе привязки к службе доставки. Основная задумка в том, чтобы можно было на странице элемента вместо ввода ID службы, выбрать метод доставки в привычном виде - с помощью dropdown или select.
В идеале надо сделать так, чтобы не нужно было для каждой сущности: доставка, оплата, группа пользователей - создавать своё пользовательское свойство. Так появилась идея делать привязку к ORM таблице.
Как видно из скриншота, можно задать модуль который автоматически подключиться, ORM класс, из которого будут получаться данные, а так же два поля: одно будет записываться в БД, а другое показываться пользователю. Небольшим дополнением является выбор внешнего вида элемента управления.
Моя реализация данной задачи это несколько классов, подключенных через autoload.
Base.php
<?php
namespace Gricuk\Iblock\EnumProperty;
use Bitrix\Main\Loader;
class Base extends \Bitrix\Main\UserField\TypeBase
{
const USER_TYPE_ID = "customList";
protected $property = NULL;
/** Интерфейсный метод, который описывает всю работу со свойством
* @return array
*/
public static function GetUserTypeDescription()
{
$params = array(
"USER_TYPE" => static::USER_TYPE_ID,
"USER_TYPE_ID" => static::USER_TYPE_ID,
"CLASS_NAME" => __CLASS__,
"DESCRIPTION" => "Кастомный список",
"BASE_TYPE" => \CUserTypeManager::BASE_TYPE_STRING,
"GetPropertyFieldHtml" => [__CLASS__, 'GetEditFormHTML'],
"GetEditFormHTML" => [__CLASS__, "GetEditFormHTML"],
"PrepareSettings" => [__CLASS__, 'PrepareSettings'],
"GetSettingsHTML" => [__CLASS__, 'GetSettingsHTML']
);
return $params;
}
public function __construct($property)
{
$this->property = $property;
}
/** Интерфейсный метод, который описывает тип поля в БД
* @param $arUserField
* @return string
*/
public static function GetDBColumnType($arUserField)
{
global $DB;
switch (strtolower($DB->type)) {
case "mysql":
return "text";
case "oracle":
return "varchar2(2000 char)";
case "mssql":
return "varchar(2000)";
}
return "text";
}
/**Подгатавливает массив настроект для записи в БД
* @param $arUserField
* @return array
*/
function PrepareSettings($arUserField)
{
$module = trim($arUserField["USER_TYPE_SETTINGS"]["MODULE"]);
$ormFieldCodeId = trim($arUserField["USER_TYPE_SETTINGS"]["ORM_FIELD_CODE_ID"]);
$ormFieldCodeName = trim($arUserField["USER_TYPE_SETTINGS"]["ORM_FIELD_CODE_NAME"]);
$ormClass = trim($arUserField["USER_TYPE_SETTINGS"]["ORM_CLASS"]);
$height = intval($arUserField["USER_TYPE_SETTINGS"]["LIST_HEIGHT"]);
$disp = $arUserField["USER_TYPE_SETTINGS"]["DISPLAY"];
$caption_no_value = trim($arUserField["USER_TYPE_SETTINGS"]["CAPTION_NO_VALUE"]);
$show_no_value = $arUserField["USER_TYPE_SETTINGS"]["SHOW_NO_VALUE"] === 'N' ? 'N' : 'Y';
if ($disp !== "CHECKBOX" && $disp !== "LIST" && $disp !== 'UI') {
$disp = "LIST";
}
return array(
"MODULE" => $module,
"ORM_CLASS" => $ormClass,
"ORM_FIELD_CODE_ID" => $ormFieldCodeId,
"ORM_FIELD_CODE_NAME" => $ormFieldCodeName,
"DISPLAY" => $disp,
"LIST_HEIGHT" => ($height < 1 ? 1 : $height),
"CAPTION_NO_VALUE" => $caption_no_value, // no default value - only in output
"SHOW_NO_VALUE" => $show_no_value, // no default value - only in output
);
}
/** Интерфейсный метод, который описывает настройки поля в настройках свойства
* @param bool $arProperty
* @param $strHTMLControlName
* @param $arPropertyFields
* @return string
*/
public static function GetSettingsHTML($arProperty = false, $strHTMLControlName, &$arPropertyFields)
{
$result = '';
$userProperties = $arProperty["USER_TYPE_SETTINGS"];
$value = $userProperties["MODULE"];
$result .= '
<tr>
<td class="adm-detail-valign-top">' . 'Модуль' . ':</td>
<td>
<input type="text" name="' . $strHTMLControlName["NAME"] . '[MODULE]" value="' . $value . '">
</td>
</tr>
';
$value = $userProperties["ORM_CLASS"];
$result .= '
<tr>
<td class="adm-detail-valign-top">' . 'ORM класс' . ':</td>
<td>
<input type="text" name="' . $strHTMLControlName["NAME"] . '[ORM_CLASS]" value="' . $value . '">
</td>
</tr>
';
if ($userProperties["ORM_FIELD_CODE_ID"]) {
$value = trim($userProperties["ORM_FIELD_CODE_ID"]);
} else {
$value = '';
}
$result .= '
<tr>
<td>' . "Код поля для ID" . ':</td>
<td>
<input type="text" name="' . $strHTMLControlName["NAME"] . '[ORM_FIELD_CODE_ID]" size="10" value="' . htmlspecialcharsbx($value) . '">
</td>
</tr>
';
if ($userProperties["ORM_FIELD_CODE_NAME"]) {
$value = trim($userProperties["ORM_FIELD_CODE_NAME"]);
} else {
$value = '';
}
$result .= '
<tr>
<td>' . "Код поля для отображения" . ':</td>
<td>
<input type="text" name="' . $strHTMLControlName["NAME"] . '[ORM_FIELD_CODE_NAME]" size="10" value="' . htmlspecialcharsbx($value) . '">
</td>
</tr>
';
if ($userProperties["DISPLAY"]) {
$value = $userProperties["DISPLAY"];
} else {
$value = "LIST";
}
$result .= '
<tr>
<td class="adm-detail-valign-top">' . "Способ отображения" . ':</td>
<td>
<label><input type="radio" name="' . $strHTMLControlName["NAME"] . '[DISPLAY]" value="LIST" ' . ("LIST" == $value ? 'checked="checked"' : '') . '>' . "Список" . '</label><br>
<label><input type="radio" name="' . $strHTMLControlName["NAME"] . '[DISPLAY]" value="CHECKBOX" ' . ("CHECKBOX" == $value ? 'checked="checked"' : '') . '>' . "Чекбоксы" . '</label><br>
<label><input type="radio" disabled name="' . $strHTMLControlName["NAME"] . '[DISPLAY]" value="UI" ' . ("UI" == $value ? 'checked="checked"' : '') . '>' . "UI" . '</label><br>
</td>
</tr>
';
if ($userProperties["LIST_HEIGHT"]) {
$value = intval($userProperties["LIST_HEIGHT"]);
} else {
$value = 5;
}
$result .= '
<tr>
<td>' . 'Высота списка' . ':</td>
<td>
<input type="text" name="' . $strHTMLControlName["NAME"] . '[LIST_HEIGHT]" size="10" value="' . $value . '">
</td>
</tr>
';
if ($userProperties["CAPTION_NO_VALUE"]) {
$value = trim($userProperties["CAPTION_NO_VALUE"]);
} else {
$value = '';
}
$result .= '
<tr>
<td>' . "Подпись при отсутствии значения" . ':</td>
<td>
<input type="text" name="' . $strHTMLControlName["NAME"] . '[CAPTION_NO_VALUE]" size="10" value="' . htmlspecialcharsbx($value) . '">
</td>
</tr>
';
if ($userProperties["CAPTION_NO_VALUE"]) {
$value = trim($userProperties["CAPTION_NO_VALUE"]);
} else {
$value = '';
}
if ($userProperties["SHOW_NO_VALUE"]) {
$value = trim($userProperties["SHOW_NO_VALUE"]);
} else {
$value = '';
}
$result .= '
<tr>
<td>' . "Показывать пустое значение для обязательного поля" . ':</td>
<td>
<input type="hidden" name="' . $strHTMLControlName["NAME"] . '[SHOW_NO_VALUE]" value="N" />
<label><input type="checkbox" name="' . $strHTMLControlName["NAME"] . '[SHOW_NO_VALUE]" value="Y" ' . ($value === 'N' ? '' : ' checked="checked"') . ' /> ' . GetMessage('MAIN_YES') . '</label>
</td>
</tr>
';
return $result;
}
/**интерфейсный метод для вывода HTML на странице редактирования элемента
* @param $arProperty
* @param $propertyValue
* @param $propertyFormCfg
* @return string
* @throws \Bitrix\Main\ArgumentException
*/
function GetEditFormHTML($arProperty, $propertyValue, $propertyFormCfg)
{
$enum = static::getEnumList($arProperty);
if (empty($enum))
return '';
$propertyFactory = new Factory();
$property = $propertyFactory->getProperty($arProperty);
$result = $property->getEditHTML($enum, $propertyValue, $propertyFormCfg);
return $result;
}
/** интерфейсный метод для вывода HTML в списке
* @param $arUserField
* @param $arHtmlControl
* @return string
*/
public static function GetFilterHTML($arUserField, $arHtmlControl)
{
if (!is_array($arHtmlControl["VALUE"]))
$arHtmlControl["VALUE"] = array();
$enum = static::getEnumList($arUserField);
if (empty($enum))
return '';
if ($arUserField["SETTINGS"]["LIST_HEIGHT"] < 5)
$size = ' size="5"';
else
$size = ' size="' . $arUserField["SETTINGS"]["LIST_HEIGHT"] . '"';
$result = '<select multiple name="' . $arHtmlControl["NAME"] . '[]"' . $size . '>';
$result .= '<option value=""' . (!$arHtmlControl["VALUE"] ? ' selected' : '') . '>' . GetMessage("MAIN_ALL") . '</option>';
foreach ($enum as $idEnum => $enumName) {
$result .= '<option value="' . $idEnum . '"' . (in_array($idEnum, $arHtmlControl["VALUE"]) ? ' selected' : '') . '>' . $enumName . '</option>';
}
$result .= '</select>';
return $result;
}
/**
* @param $arUserField
* @param $arHtmlControl
* @return array
*/
function GetFilterData($arUserField, $arHtmlControl)
{
$items = static::getEnumList($arUserField);
return array(
"id" => $arHtmlControl["ID"],
"name" => $arHtmlControl["NAME"],
"type" => "list",
"items" => $items,
"params" => array("multiple" => "Y"),
"filterable" => ""
);
}
/** Вовзращает массив из таблицы описанный в ORM_CLASS
* @param $arUserField
* @param array $arParams
* @return array
*/
protected static function getEnumList(&$arUserField, $arParams = array())
{
$enum = array();
$showNoValue = $arUserField["MANDATORY"] != "Y"
|| $arUserField['SETTINGS']['SHOW_NO_VALUE'] != 'N'
|| (isset($arParams["SHOW_NO_VALUE"]) && $arParams["SHOW_NO_VALUE"] == true);
if ($showNoValue
&& ($arUserField["SETTINGS"]["DISPLAY"] != "CHECKBOX" || $arUserField["MULTIPLE"] <> "Y")
) {
$enum = array(null => htmlspecialcharsbx(static::getEmptyCaption($arUserField)));
}
$ormFieldCodeId = $arUserField["USER_TYPE_SETTINGS"]["ORM_FIELD_CODE_ID"];
$ormFieldCodeName = $arUserField["USER_TYPE_SETTINGS"]["ORM_FIELD_CODE_NAME"];
try {
Loader::includeModule($arUserField["USER_TYPE_SETTINGS"]["MODULE"]);
} catch (\Exception $e) {
return [];
}
/** @var \Bitrix\Main\ORM\Query\Result $rsEnum */
$rsEnum = call_user_func_array(
array($arUserField["USER_TYPE_SETTINGS"]["ORM_CLASS"], "getList"),
array()
);
while ($arEnum = $rsEnum->fetch()) {
$enum[$arEnum[$ormFieldCodeId]] = $arEnum[$ormFieldCodeName];
}
$arUserField["USER_TYPE"]["FIELDS"] = $enum;
return $enum;
}
/** Возвращает то что надо выводить в случае отсутвия значения
* @param $arUserField
* @return mixed|string
*/
protected static function getEmptyCaption($arUserField)
{
return $arUserField["SETTINGS"]["CAPTION_NO_VALUE"] <> ''
? $arUserField["SETTINGS"]["CAPTION_NO_VALUE"]
: "не задано";
}
/** Возвращает HTML для вывода свойства в админке
* @param $propertyValue
* @param $propertyFormCfg
*/
protected function getEditHTML($enum, $propertyValue, $propertyFormCfg)
{
}
}
Checkbox.php
<?php
namespace Gricuk\Iblock\EnumProperty;
class Checkbox extends Base
{
public function getEditHTML($enum, $propertyValue, $propertyFormCfg)
{
$arProperty = $this->property;
$bWasSelect = false;
$result2 = '';
foreach ($enum as $idEnum => $valEnum) {
$bSelected = $propertyValue["VALUE"] == $idEnum;
$bWasSelect = $bWasSelect || $bSelected;
$result2 .= '<label><input type="radio" value="' . $idEnum . '" name="' . $propertyFormCfg["VALUE"] . '"' . ($bSelected ? ' checked' : '') . '>' . $valEnum . '</label><br>';
}
$result = $result2;
return $result;
}
}
Select.php
<?php
namespace Gricuk\Iblock\EnumProperty;
class Select extends Base
{
public function getEditHTML($enum, $propertyValue, $propertyFormCfg)
{
$arProperty = $this->property;
$bWasSelect = false;
$result2 = '';
foreach ($enum as $idEnum => $valEnum) {
$bSelected = $propertyValue["VALUE"] == $idEnum;
$bWasSelect = $bWasSelect || $bSelected;
$result2 .= '<option value="' . $idEnum . '"' . ($bSelected ? ' selected' : '') . '>' . $valEnum . '</option>';
}
if ($arProperty["SETTINGS"]["LIST_HEIGHT"] > 1) {
$size = ' size="' . $arProperty["SETTINGS"]["LIST_HEIGHT"] . '"';
} else {
$propertyValue["VALIGN"] = "middle";
$size = '';
}
$result = '<select name="' . $propertyFormCfg["VALUE"] . '"' . $size . '>';
$result .= $result2;
$result .= '</select>';
return $result;
}
}
Factory.php
<?php
namespace Gricuk\Iblock\EnumProperty;
class Factory
{
public function __construct()
{
}
public function getProperty($property)
{
switch ($property["USER_TYPE_SETTINGS"]["DISPLAY"]) {
case "CHECKBOX":
return new Checkbox($property);
case "LIST":
default:
return new Select($property);
}
}
}
Вот так подключаем все это к событию
<?
$bxEventManager = \Bitrix\Main\EventManager::getInstance();
/**
* Регистрация пользовательского свойства "Кастомный список"
*/
$bxEventManager->addEventHandler(
"iblock",
"OnIBlockPropertyBuildList",
array(
'\Gricuk\Iblock\EnumProperty\Base',
"GetUserTypeDescription",
)
);