Вот мне тоже понадобилось что-то подобное. Подумал, что не буду я переписывать умный фильтр и фасетный индекс ради этого (да и не уверен, что справлюсь). Обойдусь копированием свойств из торговых предложений в товар. Тогда, если выводить в фильтр только свойства инфоблока товаров, в фильтре будут все нужные значения и карточки фильтровать он будет более менее адекватно. (в них всё ещё будут отображаться неподходящие ТП, но хотя бы лишних карточек не будет, как и дублирования свойств). Естественно, свойства у товаров должны быть множественными, а у предложений - нет. Ну и символьные коды должны совпадать.
Получился вот такой вот класс:
<?php
use Bitrix\Iblock\IblockTable,
Bitrix\Iblock\PropertyTable,
Bitrix\Main\Loader,
Bitrix\Iblock\ORM\PropertyValue;
class CChangeElementPropertiesHandlers
{
// Обрабатываемые инфоблоки (товаров и торговых предложений)
const IBLOCKS = array(30, 31);
// Список обрабатываемых свойств
const PROPERTIES = array(
'FILTER_CODE',
'FILTER_CODE_2'
);
// Переменные каталога
static protected $SKU_PROPERTY_ID;
static protected $SKU_PROPERTY_CODE;
static protected $PRODUCT_IBLOCK_ID;
static protected $OFFER_IBLOCK_ID;
static protected $LIST_PROPERTIES;
function OnItemUpdateLinkProperties(&$arElement) {
if(!is_array($arElement) || $arElement['ID'] <= 0)
return;
if(isset($arElement['RESULT_MESSAGE']))
return;
if(!in_array($arElement['IBLOCK_ID'], self::IBLOCKS))
return;
// Если вдруг (ВДРУГ) заполнены переменные, а у нас другой инфоблок, надо переопределять
if(!in_array($arElement['IBLOCK_ID'], array(self::$OFFER_IBLOCK_ID, self::$PRODUCT_IBLOCK_ID)))
{
// Заполняем свойства каталога (инфоблок товаров, инфоблок ТП и свойство привязки)
if(!isset(self::$SKU_PROPERTY_ID))
self::initCatalogInfo($arElement['IBLOCK_ID']);
if(!isset(self::$SKU_PROPERTY_ID))
return;
}
// Сохранение ТП во всплывайке всё равно ничего не принесёт хорошего, можно дальше не продолжать
if($arElement['IBLOCK_ID'] == self::$OFFER_IBLOCK_ID && $_REQUEST['bxsender'] == 'core_window_cadmindialog')
return;
self::getSkuPropertyCode();
// Связываем все значения свойств типа Список из ТП с такими же из Товара
self::getListedProperties();
// Если это не тот инфоблок
switch($arElement['IBLOCK_ID']) {
case self::$OFFER_IBLOCK_ID:
$result = self::processOffer($arElement);
break;
case self::$PRODUCT_IBLOCK_ID:
$result = self::processProduct($arElement);
break;
default:
return;
}
if(!empty($result))
{
/* Старый добрый SetPropertyValueEx
foreach($result as $id => $values)
{
CIBlockElement::SetPropertyValuesEx($id, self::$PRODUCT_IBLOCK_ID, $values);
\Bitrix\Iblock\PropertyIndex\Manager::updateElementIndex(self::$PRODUCT_IBLOCK_ID, $id);
}
*/
// Новый добрый D7
$ids = array_keys($result);
$elementClass = \Bitrix\Iblock\Iblock::wakeUp(self::$PRODUCT_IBLOCK_ID)->getEntityDataClass();
// Добавляем .VALUE каждому свойству. Инфоблоки 2.0 не работают без него.
$arSelect = array_map('self::setSafePropertiesSelect', self::PROPERTIES);
// Берём все отслеживаемые свойства, если их нет в $result, они всё равно не изменятся
$elements = $elementClass::getList([
'select' => $arSelect,
'filter' => [
'=ID' => $ids
],
])->fetchCollection();
if(count($elements) > 0)
{
foreach($elements as $element) {
$id = $element->getId();
foreach($result[$id] as $code => $arValues)
{
// Удаляем все значения у этого свойства (свойства множественные, помним, да?)
$element->__call('removeAll', [$code]);
foreach($arValues as $value)
{
if(empty($value['VALUE']))
continue;
// Добавляем новые значения
$element->__call('addTo', [$code, new PropertyValue($value['VALUE'])]);
}
}
// Сохраняем элемент. Иначе не сохранятся значения.
$element->save();
// Фасетный индекс. Это же всё для фильтра.
\Bitrix\Iblock\PropertyIndex\Manager::updateElementIndex(self::$PRODUCT_IBLOCK_ID, $id);
}
}
}
}
private function processOffer(&$arElement) {
// Если свойства не передавались (значит не изменялись)
if(!isset($arElement['PROPERTY_VALUES']))
return false;
// Значения свойств SKU
$arOffersValues = self::getOffersValues($arElement);
if(empty($arOffersValues))
return false;
// Значения свойств товара
$arProductValues = self::getProductValues($arElement);
if(empty($arProductValues))
return false;
$result = self::getResultArray($arProductValues, $arOffersValues);
if(empty($result))
return false;
return $result;
}
private function processProduct(&$arElement) {
// Значения свойств SKU
$arOffersValues = self::getOffersValues($arElement);
if(empty($arOffersValues))
return false;
// Значения свойств товара
$arProductValues = self::getProductValues($arElement);
if(empty($arProductValues))
return false;
$result = self::getResultArray($arProductValues, $arOffersValues);
if(empty($result))
return false;
return $result;
}
private function getSkuPropertyCode()
{
$parameters = [
'select' => [
'CODE'
],
'filter' => [
'=IBLOCK_ID' => self::$OFFER_IBLOCK_ID,
'=ID' => self::$SKU_PROPERTY_ID
]
];
$arProp = PropertyTable::getRow($parameters);
if($arProp)
self::$SKU_PROPERTY_CODE = $arProp['CODE']; // Код свойства связанного товара
}
private function getListedProperties() {
if(!empty(self::$LIST_PROPERTIES))
return;
self::$LIST_PROPERTIES['_codes'] = array();
$tempProductList = array();
$tempProductPropertyIds = array();
$tempOffersList = array();
$tempOffersValues = array();
$tempOffersProperties = array();
$parameters = [
'select' => [
'ID', 'CODE', 'IBLOCK_ID', 'PROPERTY_TYPE'
],
'filter' => [
'PROPERTY_TYPE' => PropertyTable::TYPE_LIST,
'CODE' => self::PROPERTIES
],
'order' => ['SORT']
];
$dbProp = PropertyTable::getList($parameters);
$arProp = $dbProp->fetchAll();
foreach($arProp as $prop)
{
// Массив с инфоблоками позже позволит нам понять, где что
$_iblocks[$prop['ID']] = $prop['IBLOCK_ID'];
// Собираем коды свойств типа Список, чтобы проверять при назначении значения
self::$LIST_PROPERTIES['_codes'][$prop['ID']] = $prop['CODE'];
// Тут будут значения
self::$LIST_PROPERTIES[$prop['CODE']] = array();
}
if(!empty(self::$LIST_PROPERTIES['_codes']))
{
$dbPropValues = \Bitrix\Iblock\PropertyEnumerationTable::getList(array(
'filter' => array('PROPERTY_ID'=>array_keys($_iblocks)),
));
$arPropValues = $dbPropValues->fetchAll();
foreach($arPropValues as $arEnum)
{
$code = self::$LIST_PROPERTIES['_codes'][$arEnum['PROPERTY_ID']];
if($_iblocks[$arEnum['PROPERTY_ID']] == self::$PRODUCT_IBLOCK_ID)
{
$tempProductList[$code][$arEnum['XML_ID']] = $arEnum['ID'];
$tempProductPropertyIds[$code] = $arEnum['PROPERTY_ID'];
} else {
$tempOffersList[$code][$arEnum['ID']] = $arEnum['XML_ID'];
$tempOffersValues[$code][$arEnum['ID']] = $arEnum['VALUE'];
$tempOffersProperties[$code][$arEnum['ID']] = [
'XML_ID' => $arEnum['XML_ID'],
'SORT' => $arEnum['SORT'],
'VALUE' => $arEnum['VALUE']
];
}
}
foreach($tempOffersList as $code => $arValues)
{
// Если свойство у Товара тоже список, то делаем соответствие ID
if(isset($tempProductList[$code]))
{
foreach($arValues as $id => $xml)
{
if(!empty($tempProductList[$code][$xml]))
{
// Если у свойства Товара есть такое значение, то пишем его
self::$LIST_PROPERTIES[$code][$id] = $tempProductList[$code][$xml];
} else {
// Иначе - создаём новое значение
$tempOffersProperties[$code][$id]['PROPERTY_ID'] = $tempProductPropertyIds[$code];
/* Херас два, оно не работает, когда XML_ID совпадают. Даже у разных свойств. D7 такой D7 */
//$dbRes = \Bitrix\Iblock\PropertyEnumerationTable::add($tempOffersProperties[$code][$id]);
$propertiesEnumerationNewId = CIBlockPropertyEnum::Add($tempOffersProperties[$code][$id]);
if($propertiesEnumerationNewId > 0)
self::$LIST_PROPERTIES[$code][$id] = $propertiesEnumerationNewId;
}
}
} else {
// Иначе - значения
self::$LIST_PROPERTIES[$code] = $tempOffersValues[$code];
}
}
}
}
private function getPropertiesKeys($iblockId, $keyWord) {
$arKeys = [];
$parameters = [
'select' => [
'ID', 'CODE', 'IBLOCK_ID', 'MULTIPLE', 'PROPERTY_TYPE'
],
'filter' => [
'IBLOCK_ID' => $iblockId,
'CODE' => self::PROPERTIES
],
'order' => ['SORT']
];
// У инфоблока Товаров используем только множественные свойства, у инфоблока ТП - только НЕ множественные
$needMultiple = $iblockId == self::$PRODUCT_IBLOCK_ID ? 'Y' : 'N';
$dbProp = PropertyTable::getList($parameters);
$arProp = $dbProp->fetchAll();
foreach($arProp as $prop)
{
if($prop['PROPERTY_TYPE'] !== PropertyTable::TYPE_NUMBER
&& $prop['PROPERTY_TYPE'] !== PropertyTable::TYPE_STRING
&& $prop['PROPERTY_TYPE'] !== PropertyTable::TYPE_LIST)
continue;
if ($prop['MULTIPLE'] == $needMultiple)
$arKeys[$prop['CODE']] = $prop[$keyWord]; // Записываем ключи обрабатываемых свойств в пришедшем массиве
}
return $arKeys;
}
private function getOffersValues($arElement) {
$arOffersValues = [];
// Если это ТП, то мы можем игнорировать вариант, когда свойства не передавались. Потому что тогда ничего не поменялось.
if($arElement['IBLOCK_ID'] == self::$OFFER_IBLOCK_ID)
{
// Свойства могли передать как ID и как CODE. Надо это узнать.
$keyWord = is_int(array_key_first($arElement['PROPERTY_VALUES'])) ? 'ID' : 'CODE';
// Ищем ключи свойств в принимаемом массиве
$PROPERTIES_KEYS = self::getPropertiesKeys($arElement['IBLOCK_ID'], $keyWord);
// Вытаскиваем все значения обрабатываемых свойств.
foreach($PROPERTIES_KEYS as $code => $key)
{
// Только те, которые передавались. Не изменившиеся ни на что не влияют
if(!empty($arElement['PROPERTY_VALUES'][$key]))
{
$arOffersValues[$code] = [];
// Без понятия по какому принципу это массив или не массив. Иногда, всё в массивах. Иногда, что-то строка. Но один фиг оно мне всё надо.
if(is_array($arElement['PROPERTY_VALUES'][$key]))
{
foreach($arElement['PROPERTY_VALUES'][$key] as $valueId => $elValue)
{
if(is_array($elValue) && !empty($elValue['VALUE']))
{
// если есть свойства типа список, среди них есть это свойство и есть соответствующее значение из инфоблока Товаров
if(is_array(self::$LIST_PROPERTIES['_codes'])
&& in_array($code, self::$LIST_PROPERTIES['_codes'])
&& !empty(self::$LIST_PROPERTIES[$code][$elValue['VALUE']]))
{
$arOffersValues[$code][] = self::$LIST_PROPERTIES[$code][$elValue['VALUE']];
} else {
$arOffersValues[$code][] = $elValue['VALUE'];
}
}
}
} else {
// если есть свойства типа список, среди них есть это свойство и есть соответствующее значение из инфоблока Товаров
if (is_array(self::$LIST_PROPERTIES['_codes'])
&& in_array($code, self::$LIST_PROPERTIES['_codes'])
&& !empty(self::$LIST_PROPERTIES[$code][$arElement['PROPERTY_VALUES'][$key]]))
{
$arOffersValues[$code][] = self::$LIST_PROPERTIES[$code][$arElement['PROPERTY_VALUES'][$key]];
} else {
$arOffersValues[$code][] = $arElement['PROPERTY_VALUES'][$key];
}
}
}
}
}
else
{
// Если передан товар, берём значения из БД
// Инициируем класс инфоблока SKU
$elementClass = \Bitrix\Iblock\Iblock::wakeUp(self::$OFFER_IBLOCK_ID)->getEntityDataClass();
// Добавляем .VALUE каждому свойству. Инфоблоки 2.0 не работают без него.
$arSelect = array_map('self::setSafePropertiesSelect', self::PROPERTIES);
// TMP_ID > 0 при создании товара. Если создаётся с ТП, то и у ТП такие же TMP_ID
if($arElement['TMP_ID'])
{
$elements = $elementClass::getList([
'select' => $arSelect,
'filter' => [
'=IBLOCK_ID' => self::$OFFER_IBLOCK_ID,
'=ACTIVE' => 'Y',
'=TMP_ID' => $arElement['TMP_ID'],
],
'order' => ['SORT'],
])->fetchCollection();
} else {
$elements = $elementClass::getList([
'select' => $arSelect,
'filter' => [
'=IBLOCK_ID' => self::$OFFER_IBLOCK_ID,
'=ACTIVE' => 'Y',
'='.self::$SKU_PROPERTY_CODE.'.VALUE' => $arElement['ID']
],
'order' => ['SORT'],
])->fetchCollection();
}
// Достаём все товары
foreach($elements as $element)
{
foreach(self::PROPERTIES as $code)
{
if(!isset($arOffersValues[$code]))
$arOffersValues[$code] = [];
if($dbProperty = $element->__call('get', [$code]))
{
if (!method_exists($dbProperty, 'getAll'))
{
$value = $dbProperty->getValue();
// если есть свойства типа список, среди них есть это свойство и есть соответствующее значение из инфоблока Товаров
if(is_array(self::$LIST_PROPERTIES['_codes']) && in_array($code, self::$LIST_PROPERTIES['_codes']) && !empty(self::$LIST_PROPERTIES[$code][$value]))
{
$arOffersValues[$code][] = self::$LIST_PROPERTIES[$code][$value];
} else {
$arOffersValues[$code][] = $value;
}
} else {
// Не вижу смысла собирать множественные свойства из ТП. Но если вдруг, то вот они (недоделанные)
/*
foreach ($dbProperty->getAll() as $value) {
$elPropertyValues[$code] = $value->getValue();
}
*/
}
}
}
}
foreach(self::PROPERTIES as $code)
{
if(is_array($arOffersValues[$code]))
$arOffersValues[$code] = array_unique($arOffersValues[$code]);
}
}
return $arOffersValues;
}
private function getProducts($arElement) {
// Если это товар, то можем сразу вернуть его ID
if($arElement['IBLOCK_ID'] == self::$PRODUCT_IBLOCK_ID)
return [$arElement['ID']];
$arProducts = [];
$skuKey = self::$SKU_PROPERTY_ID;
if(!$arElement['PROPERTY_VALUES'][$skuKey])
$skuKey = self::$SKU_PROPERTY_CODE;
if($arElement['PROPERTY_VALUES'][$skuKey])
{
// Если среди переданных свойств есть свойство привязки, то берём инфу оттуда
if(is_array($arElement['PROPERTY_VALUES'][$skuKey]))
{
foreach($arElement['PROPERTY_VALUES'][$skuKey] as $code => $arValue)
{
if(is_array($arValue))
{
if($arValue['VALUE'] !== NULL)
$arProducts[] = $arValue['VALUE'];
} else {
$arProducts[] = $arValue;
}
}
} else {
$arProducts[] = $arElement['PROPERTY_VALUES'][$skuKey];
}
} else {
// Если среди переданных свойств нет свойства привязки, то берём инфу из БД
// Инициируем класс инфоблока SKU
$elementClass = \Bitrix\Iblock\Iblock::wakeUp(self::$OFFERS_IBLOCK_ID)->getEntityDataClass();
$arSelect = [self::$SKU_PROPERTY_CODE.'.VALUE'];
$elements = $elementClass::getList([
'select' => $arSelect,
'filter' => [
'=ID' => $arElement['ID'],
]
])->fetchCollection();
// Достаём все товары
foreach($elements as $element)
{
if($dbProperty = $element->__call('get', [self::$SKU_PROPERTY_CODE]))
{
if (method_exists($dbProperty, 'getAll'))
{
foreach ($dbProperty->getAll() as $value) {
$arProducts[] = $value->getValue();
}
} else {
$arProducts[] = $dbProperty->getValue();
}
}
}
}
return $arProducts;
}
/**
* Получаем заполненные значения свойств товара
*
* @param array $arElement переданный элемент
*
* @return array Массив обрабатываемых значений свойств товаров.
* Структура: ID товара -> Символьный код свойства -> Массив значений
*/
private function getProductValues($arElement) {
// Вытаскиваем ID товаров
$arProducts = self::getProducts($arElement);
// Если товаров нет (например, ТП ни к чему не привязано), то пiхуй
if(empty($arProducts))
return [];
$arProductValues = [];
// Свойства, значения которых нужно доставать из базы
$propertiesFromDB = self::PROPERTIES;
if($arElement['IBLOCK_ID'] == self::$PRODUCT_IBLOCK_ID)
{
if(isset($arElement['PROPERTY_VALUES']))
{
$existingProperties = [];
$keyWord = is_int(array_key_first($arElement['PROPERTY_VALUES'])) ? 'ID' : 'CODE';
$PROPERTIES_KEYS = self::getPropertiesKeys($arElement['IBLOCK_ID'], $keyWord);
foreach($PROPERTIES_KEYS as $code => $key)
{
$arProductValues[$arElement['ID']][$code] = [];
if(isset($arElement['PROPERTY_VALUES'][$key]))
{
$existingProperties[] = $code;
foreach($arElement['PROPERTY_VALUES'][$key] as $valueId => $elValue)
{
// Из формы редактирования $elValue всегда массив, а из списка товаров - строка
if(is_array($elValue))
{
if(!empty($elValue['VALUE']))
$arProductValues[$arElement['ID']][$code][] = $elValue['VALUE'];
} else {
$arProductValues[$arElement['ID']][$code][] = $elValue;
}
}
}
}
if(!empty($existingProperties))
{
$propertiesFromDB = array_diff($propertiesFromDB, $existingProperties);
}
}
}
if(!empty($propertiesFromDB))
{
// Если переданный элемент, это ТП, то тащим значения из базы
// Инициируем класс инфоблока товаров
$elementClass = \Bitrix\Iblock\Iblock::wakeUp(self::$PRODUCT_IBLOCK_ID)->getEntityDataClass();
// Добавляем .VALUE каждому свойству. Инфоблоки 2.0 не работают без него.
$arSelect = array_map('self::setSafePropertiesSelect', $propertiesFromDB);
$arSelect = array_merge(['ID'], $arSelect);
$elements = $elementClass::getList([
'select' => $arSelect,
'filter' => [
'ID' => $arProducts,
]
])->fetchCollection();
// Массив значений изменяемых свойств у товаров
foreach($elements as $element)
{
foreach($propertiesFromDB as $code){
$arProductValues[$element->getId()][$code] = [];
// Если свойство заполнено
if($dbProperty = $element->__call('get', [$code]))
{
// getAll есть только у множественных, а нам только они и нужны
if (method_exists($dbProperty, 'getAll'))
{
foreach($dbProperty->getAll() as $value)
{
// ID товара -> Символьный код свойства -> Значение
$arProductValues[$element->getId()][$code][] = $value->getValue();
}
}
}
}
}
}
return $arProductValues;
}
private function getResultArray($arProductValues, $arOffersValues)
{
$result = [];
foreach($arProductValues as $id => $properties)
{
foreach($properties as $code => $values)
{
// Если свойства в ТП не заполнено, то и делать ничего не надо с ним
// А если заполнено
if(!empty($arOffersValues[$code]))
{
// Объединяем значения из ТП и из товара, удаляем дубли, и если в товаре сейчас не это, надо менять.
$arTempValues = array_merge($values, $arOffersValues[$code]);
$arTempValues = array_unique($arTempValues);
if($arTempValues != $values)
{
foreach($arTempValues as $value)
{
$result[$id][$code][] = ['VALUE' => $value, 'DESCRIPTION' => ''];
}
}
}
}
}
return $result;
}
private function initCatalogInfo($iblockId)
{
if (!Loader::includeModule("catalog"))
return;
$productIBlock = CCatalogSKU::GetInfoByOfferIBlock($iblockId);
if(!$productIBlock)
$productIBlock = CCatalogSKU::GetInfoByProductIBlock($iblockId);
if($productIBlock)
{
self::$SKU_PROPERTY_ID = $productIBlock['SKU_PROPERTY_ID'];
self::$PRODUCT_IBLOCK_ID = $productIBlock['PRODUCT_IBLOCK_ID'];
self::$OFFER_IBLOCK_ID = $productIBlock['IBLOCK_ID'];
}
}
private function setSafePropertiesSelect($value)
{
return $value.'.VALUE';
}
}
// array_key_first появился в PHP 7.3.0
// При обновлении версии, можно будет удалить
if (!function_exists('array_key_first')) {
function array_key_first(array $arr) {
foreach($arr as $key => $unused) {
return $key;
}
return NULL;
}
}
Как видите, названия классов - не моё И скорее всего не очень оптимально местами. Но стараюсь лишний раз к базе не обращаться. Поэтому и список свойств хардкодится, а не формируется автоматически. Хотя по-хорошему и для удобства пользования надо вынести это всё в модуль с настройками и оттуда брать.
Подключается на события OnAfterIBlockElementUpdate иOnAfterIBlockElementAdd
У обрабатываемых инфоблоков обязательно должен быть заполнен "Символьный код API". Потому как работа с инфоблоками идёт через D7
Проверял только на типах свойств "Строка", "Число", "Справочник" и "Список".
Поле DESCRIPTION у значения очищается. Я нигде его не использую, поэтому так.
Ещё раз: обрабатываемые свойства должны иметь одинаковые символьные коды
Ещё раз 2: свойства в инфоблоке товаров - множественные. В инфоблоке торговых предложений - нет. Это тоже обязательно.
"Справочники", очевидно, у обоих свойств должны быть общие.
Свойство "Список" из торгового предложения спокойно переносится в свойство "Строка" или "Число" в карточку.
Если свойство "список" из ТП синхронизируется со свойством "Список" из карточки, то ищется совпадение по XML_ID
Если такого совпадения нет, в свойстве инфоблока товаров создаётся новое значение.
Значения заданные в карточке не удаляются при изменении торговых предложений. Только добавляются новые, если отсутствуют
Если вы изменяете торговое предложение из товара (во всплывающем окне), а потом не сохраняете товар (например нажали Отменить) – свойства из ТП не сохранятся в товаре. Это сделано для того, чтобы лишний раз не вызывать обработчик при сохранении и того, и другого.
Ну вроде всё. Код вроде достаточно откомментирован, можно всё понять. Самое главное, что почти всё получилось сделать через D7. Даже SetPropertyValuesEx вполне себе заменяется (но не будут срабатывать события вроде OnAfterIBlockElementSetPropertyValuesEx). Кроме добавления нового значения в свойство типа "Список" (пока у разных свойств не добавляются значения с одинаковым XML_ID через D7). И кроме CCatalogSKU::GetInfoByOfferIBlock и CCatalogSKU::GetInfoByProductIBlock (просто не хотелось).
(очередная никому не нужная муть, на которую ушло непозволительно много времени, хоть сохраню себе для подсматривания на будущее)
Изменено: добавил теоретическую поддержку Инфоблоков 2.0. Почему теоретическую? Потому что тестил не весь код на инфоблоках 2.0, а только часть, отвечающую за выборку. Там получается как: вот такой код сработает на Инфоблоках 1.0, но выдаст ошибку на 2.0:
Группы на сайте создаются не только сотрудниками «1С-Битрикс», но и партнерами компании. Поэтому мнения участников групп могут не совпадать с позицией компании «1С-Битрикс».