<?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;
}
}
|