Вот мне тоже понадобилось что-то подобное. Подумал, что не буду я переписывать умный фильтр и фасетный индекс ради этого (да и не уверен, что справлюсь). Обойдусь копированием свойств из торговых предложений в товар. Тогда, если выводить в фильтр только свойства инфоблока товаров, в фильтре будут все нужные значения и карточки фильтровать он будет более менее адекватно. (в них всё ещё будут отображаться неподходящие ТП, но хотя бы лишних карточек не будет, как и дублирования свойств). Естественно, свойства у товаров должны быть множественными, а у предложений - нет. Ну и символьные коды должны совпадать.
Получился вот такой вот класс:
<?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
$eventManager = \Bitrix\Main\EventManager::getInstance(); $eventManager->addEventHandler( "iblock", "OnAfterIBlockElementUpdate", array("CChangeElementPropertiesHandlers", "OnItemUpdateLinkProperties"), false, 600 ); $eventManager->addEventHandler( "iblock", "OnAfterIBlockElementAdd", array("CChangeElementPropertiesHandlers","OnItemUpdateLinkProperties"), false, 600 ); |
Из "нюансов":
- У обрабатываемых инфоблоков обязательно должен быть заполнен "Символьный код API". Потому как работа с инфоблоками идёт через D7
- Проверял только на типах свойств "Строка", "Число", "Справочник" и "Список".
- Поле DESCRIPTION у значения очищается. Я нигде его не использую, поэтому так.
- Ещё раз: обрабатываемые свойства должны иметь одинаковые символьные коды
- Ещё раз 2: свойства в инфоблоке товаров - множественные. В инфоблоке торговых предложений - нет. Это тоже обязательно.
- "Справочники", очевидно, у обоих свойств должны быть общие.
- Свойство "Список" из торгового предложения спокойно переносится в свойство "Строка" или "Число" в карточку.
- Если свойство "список" из ТП синхронизируется со свойством "Список" из карточки, то ищется совпадение по XML_ID
- Если такого совпадения нет, в свойстве инфоблока товаров создаётся новое значение.
- Значения заданные в карточке не удаляются при изменении торговых предложений. Только добавляются новые, если отсутствуют
- Если вы изменяете торговое предложение из товара (во всплывающем окне), а потом не сохраняете товар (например нажали Отменить) – свойства из ТП не сохранятся в товаре. Это сделано для того, чтобы лишний раз не вызывать обработчик при сохранении и того, и другого.
Самое главное, что почти всё получилось сделать через D7. Даже SetPropertyValuesEx вполне себе заменяется (но не будут срабатывать события вроде OnAfterIBlockElementSetPropertyValuesEx). Кроме добавления нового значения в свойство типа "Список" (пока у разных свойств не добавляются значения с одинаковым XML_ID через D7). И кроме CCatalogSKU::GetInfoByOfferIBlock и CCatalogSKU::GetInfoByProductIBlock (просто не хотелось).
(очередная никому не нужная муть, на которую ушло непозволительно много времени, хоть сохраню себе для подсматривания на будущее)
Изменено: добавил теоретическую поддержку Инфоблоков 2.0.
Почему теоретическую? Потому что тестил не весь код на инфоблоках 2.0, а только часть, отвечающую за выборку.
Там получается как: вот такой код сработает на Инфоблоках 1.0, но выдаст ошибку на 2.0:
$elementClass = \Bitrix\Iblock\Iblock::wakeUp($IBLOCK_ID)->getEntityDataClass(); $elements = $elementClass::getList([ 'select' => ['PROPERTY_CODE'], 'filter' => [ '=ID' => $ELEMENT_ID ], ])->fetchCollection(); |
$elementClass = \Bitrix\Iblock\Iblock::wakeUp($IBLOCK_ID)->getEntityDataClass(); $elements = $elementClass::getList([ 'select' => ['PROPERTY_CODE.VALUE'], 'filter' => [ '=ID' => $ELEMENT_ID ], ])->fetchCollection(); |