27 parent::__construct();
29 $this->options[
'iblockId'] = (int)(
$options[
'iblockId'] ?? 0);
30 $this->options[
'basePriceId'] = (int)(
$options[
'basePriceId'] ?? 0);
31 $this->options[
'currency'] =
$options[
'currency'] ??
'';
32 if (isset(
$options[
'restrictedProductTypes']) && is_array(
$options[
'restrictedProductTypes']))
34 $this->options[
'restrictedProductTypes'] =
$options[
'restrictedProductTypes'];
38 $this->options[
'restrictedProductTypes'] =
null;
41 $this->options[
'showPriceInCaption'] = (bool)(
$options[
'showPriceInCaption'] ??
true);
49 !$USER->isAuthorized()
50 || !AccessController::getCurrent()->check(ActionDictionary::ACTION_CATALOG_READ)
70 $items[] = $this->
makeItem($product);
93 $recentItems = $dialog->
getRecentItems()->getEntityItems(static::ENTITY_ID);
94 $recentItemsCount = count($recentItems);
96 $this->options[
'restrictedProductTypes']
97 && is_array($this->options[
'restrictedProductTypes'])
98 && $recentItemsCount > 0
102 foreach ($recentItems as $recentItem)
104 $ids[] = $recentItem->getId();
109 $restrictedTypes = array_flip($this->options[
'restrictedProductTypes']);
111 foreach ($products as $sku)
113 if (!isset($restrictedTypes[$sku[
'TYPE']]))
115 $selectedIds[] = $sku[
'ID'];
121 $selectedIds = array_flip($selectedIds);
125 foreach ($recentItems as $recentItem)
127 if (!isset($selectedIds[$recentItem->getId()]))
129 $recentItem->setAvailable(
false);
135 if ($recentItemsCount < self::PRODUCT_LIMIT)
149 if (!empty($products))
151 foreach ($products as $product)
167 $customData = array_filter(array_intersect_key($product, [
168 'SKU_PROPERTIES' =>
true,
169 'SEARCH_PROPERTIES' =>
true,
170 'PREVIEW_TEXT' =>
true,
171 'DETAIL_TEXT' =>
true,
172 'PARENT_NAME' =>
true,
173 'PARENT_SEARCH_PROPERTIES' =>
true,
174 'PARENT_PREVIEW_TEXT' =>
true,
175 'PARENT_DETAIL_TEXT' =>
true,
180 'id' => $product[
'ID'],
181 'entityId' => static::ENTITY_ID,
182 'title' => $product[
'NAME'],
183 'supertitle' => $product[
'SKU_PROPERTIES'],
186 'avatar' => $product[
'IMAGE'],
187 'customData' => $customData,
193 return $product[
'BARCODE'] ??
'';
198 if (!$this->shouldDisplayPriceInCaption())
204 'text' => $product[
'PRICE'],
214 private function getBasePriceId()
219 private function getCurrency()
224 private function shouldDisplayPriceInCaption()
226 return $this->
getOptions()[
'showPriceInCaption'];
231 static $iblockInfo =
null;
233 if ($iblockInfo ===
null)
235 $iblockInfo = ServiceContainer::getIblockInfo($this->
getIblockId());
241 private function getImageSource(
int $id): ?string
248 $file = \CFile::GetFileArray($id);
254 return Tools::getImageSrc($file,
false) ?:
null;
259 [$productIds, $offerIds] = $this->separateOffersFromProducts($ids);
262 'filter' => [
'=ID' => $productIds],
263 'offer_filter' => [
'=ID' => $offerIds],
268 return $this->sortProductsByIds($products, $productIds);
271 private function separateOffersFromProducts(array $ids): array
282 if ($iblockInfo->canHaveSku())
284 $productList = \CCatalogSku::getProductList($ids, $iblockInfo->getSkuIblockId());
285 if (!empty($productList))
290 foreach ($ids as $id)
292 if ($counter >= self::PRODUCT_LIMIT)
297 if (isset($productList[$id]))
299 $productId = $productList[$id][
'ID'];
301 if (isset($productIds[$productId]))
307 $productIds[$productId] = $productId;
311 $productIds[$id] = $id;
317 $productIds = array_values($productIds);
321 return [$productIds, $offerIds];
324 private function sortProductsByIds(array $products, array $ids): array
328 foreach ($ids as $id)
330 if (isset($products[$id]))
332 $sorted[$id] = $products[$id];
350 if ($searchString !==
'')
352 $simpleProductFilter = [
353 '*SEARCHABLE_CONTENT' => $searchString,
356 if ($iblockInfo->canHaveSku())
360 '*SEARCHABLE_CONTENT' => $searchString,
361 '=ID' => \CIBlockElement::SubQuery(
'PROPERTY_' . $iblockInfo->getSkuPropertyId(), [
362 'CHECK_PERMISSIONS' =>
'Y',
363 'MIN_PERMISSION' =>
'R',
365 'ACTIVE_DATE' =>
'Y',
366 'IBLOCK_ID' => $iblockInfo->getSkuIblockId(),
367 '*SEARCHABLE_CONTENT' => $searchString,
371 $offerFilter = $simpleProductFilter;
375 $productFilter = $simpleProductFilter;
380 'filter' => $productFilter,
381 'offer_filter' => $offerFilter,
382 'searchString' => $searchString,
394 $productFilter = (array)($parameters[
'filter'] ?? []);
395 $offerFilter = (array)($parameters[
'offer_filter'] ?? []);
396 $shouldLoadOffers = (bool)($parameters[
'load_offers'] ??
true);
398 $additionalProductFilter = [
'IBLOCK_ID' => $iblockInfo->getProductIblockId()];
400 if ($this->options[
'restrictedProductTypes'] !==
null)
402 $filteredTypes = array_intersect(
403 $this->options[
'restrictedProductTypes'],
408 $additionalProductFilter[
'!=TYPE'] = array_values(array_unique($filteredTypes));
411 'filter' => array_merge($productFilter, $additionalProductFilter),
412 'limit' => self::PRODUCT_LIMIT,
414 if (empty($products))
419 $products = $this->
loadProperties($products, $iblockInfo->getProductIblockId(), $iblockInfo);
421 if ($shouldLoadOffers && $iblockInfo->canHaveSku())
423 $products = $this->loadOffers($products, $iblockInfo, $offerFilter);
428 if (!empty($parameters[
'searchString']))
430 $products = $this->
loadBarcodes($products, $parameters[
'searchString']);
440 $additionalFilter = (array)($parameters[
'filter'] ?? []);
441 $limit = (int)($parameters[
'limit'] ?? 0);
444 'CHECK_PERMISSIONS' =>
'Y',
445 'MIN_PERMISSION' =>
'R',
447 'ACTIVE_DATE' =>
'Y',
455 $selectFields = array_filter(array_unique(array_merge(
468 array_keys($additionalFilter)
474 'nTopCount' => $limit,
478 $elementIterator = \CIBlockElement::GetList(
480 array_merge($filter, $additionalFilter),
485 while ($row = $elementIterator->Fetch())
487 $row[
'ID'] = (int)$row[
'ID'];
488 $row[
'IBLOCK_ID'] = (int)$row[
'IBLOCK_ID'];
490 $elements[$row[
'ID']] = $row;
492 unset($row, $elementIterator);
494 if (!empty($elements))
496 $elementIterator = \CIBlockElement::GetList(
499 'ID' => array_keys($elements)
505 while ($row = $elementIterator->Fetch())
507 $id = (int)$row[
'ID'];
508 unset($row[
'ID'], $row[
'IBLOCK_ID']);
510 $row[
'TYPE'] = (int)$row[
'TYPE'];
511 $row[
'IMAGE'] =
null;
512 $row[
'PRICE'] =
null;
513 $row[
'SKU_PROPERTIES'] =
null;
515 if (!empty($row[
'PREVIEW_PICTURE']))
517 $row[
'IMAGE'] = $this->getImageSource((
int)$row[
'PREVIEW_PICTURE']);
520 if (empty($row[
'IMAGE']) && !empty($row[
'DETAIL_PICTURE']))
522 $row[
'IMAGE'] = $this->getImageSource((
int)$row[
'DETAIL_PICTURE']);
525 if (!empty($row[
'PREVIEW_TEXT']) && $row[
'PREVIEW_TEXT_TYPE'] ===
'html')
527 $row[
'PREVIEW_TEXT'] = HTMLToTxt($row[
'PREVIEW_TEXT']);
530 if (!empty($row[
'DETAIL_TEXT']) && $row[
'DETAIL_TEXT_TYPE'] ===
'html')
532 $row[
'DETAIL_TEXT'] = HTMLToTxt($row[
'DETAIL_TEXT']);
535 $elements[$id] += $row;
537 unset($row, $elementIterator);
543 private function loadOffers(array $products,
IblockInfo $iblockInfo, array $additionalFilter = []): array
545 $productsWithOffers = $this->filterProductsWithOffers($products);
546 if (empty($productsWithOffers))
554 $skuPropertyId => array_keys($productsWithOffers),
556 if (!empty($additionalFilter))
558 $offerFilter = array_merge($offerFilter, $additionalFilter);
562 $offers = $this->
loadElements([
'filter' => $offerFilter]);
563 $offers = array_column($offers,
null, $skuPropertyId .
'_VALUE');
565 $productsStillWithoutOffers = array_diff_key($productsWithOffers, $offers);
566 if (!empty($productsStillWithoutOffers))
572 $skuPropertyId => array_keys($productsStillWithoutOffers),
575 $additionalOffers = array_column($additionalOffers,
null, $skuPropertyId .
'_VALUE');
576 $offers = array_merge($offers, $additionalOffers);
581 $offers = array_column($offers,
null,
'ID');
583 $products = $this->matchProductOffers($products, $offers, $skuPropertyId .
'_VALUE');
591 if (empty($elements))
596 $variationToProductMap = [];
597 foreach ($elements as $id => $element)
599 $variationToProductMap[$element[
'ID']] = $id;
603 'select' => [
'PRICE',
'CURRENCY',
'PRODUCT_ID'],
605 'PRODUCT_ID' => array_keys($variationToProductMap),
606 '=CATALOG_GROUP_ID' => $this->getBasePriceId(),
609 '<=QUANTITY_FROM' => 1,
610 '=QUANTITY_FROM' =>
null,
614 '>=QUANTITY_TO' => 1,
615 '=QUANTITY_TO' =>
null,
620 while ($price = $priceTableResult->fetch())
622 $productId = $variationToProductMap[$price[
'PRODUCT_ID']];
624 $priceValue = $price[
'PRICE'];
625 $currency = $price[
'CURRENCY'];
626 if (!empty($this->getCurrency()) && $this->getCurrency() !== $currency)
628 $priceValue = \CCurrencyRates::ConvertCurrency($priceValue, $currency, $this->getCurrency());
629 $currency = $this->getCurrency();
632 $formattedPrice = \CCurrencyLang::CurrencyFormat($priceValue, $currency,
true);
633 $elements[$productId][
'PRICE'] = $formattedPrice;
639 protected function loadBarcodes(array $elements,
string $searchString): array
641 if (empty($elements))
646 $variationToProductMap = [];
647 foreach ($elements as $id => $element)
649 $element[
'BARCODE'] =
null;
650 $variationToProductMap[$element[
'ID']] = $id;
653 $variationIds = array_keys($variationToProductMap);
655 if (empty($variationIds))
660 $barcodeRaw = \Bitrix\Catalog\StoreBarcodeTable::getList([
662 '=PRODUCT_ID' => $variationIds,
663 'BARCODE' => $searchString .
'%'
665 'select' => [
'BARCODE',
'PRODUCT_ID']
668 while ($barcode = $barcodeRaw->fetch())
670 $variationId = $barcode[
'PRODUCT_ID'];
671 $productId = $variationToProductMap[$variationId];
673 if (!isset($productId))
678 $elements[$productId][
'BARCODE'] = $barcode[
'BARCODE'];
684 private function matchProductOffers(array $products, array $offers,
string $productLinkProperty): array
686 foreach ($offers as $offer)
688 $productId = $offer[$productLinkProperty] ??
null;
690 if ($productId && isset($products[$productId]))
692 $fieldsToSafelyMerge = [
701 foreach ($fieldsToSafelyMerge as $field)
703 $products[$productId][
'PARENT_' . $field] = $products[$productId][$field] ??
null;
704 $products[$productId][$field] = $offer[$field] ??
null;
707 if (!empty($offer[
'IMAGE']))
709 $products[$productId][
'IMAGE'] = $offer[
'IMAGE'];
712 if (!empty($offer[
'SKU_PROPERTIES']))
714 $products[$productId][
'SKU_PROPERTIES'] = $offer[
'SKU_PROPERTIES'];
722 private function filterProductsWithOffers(array $products): array
726 static function ($item)
735 if (empty($elements))
742 $skuTreeProperties =
null;
745 $skuTreeProperties = array_map(
747 PropertyCatalogFeature::getOfferTreePropertyCodes($iblockId) ?? []
749 if (!empty($skuTreeProperties))
751 $propertyIds = array_merge($propertyIds, $skuTreeProperties);
755 $morePhotoId = $this->getMorePhotoPropertyId($iblockId);
758 $propertyIds[] = $morePhotoId;
761 $searchProperties = $this->getSearchPropertyIds($iblockId);
762 if (!empty($searchProperties))
764 $propertyIds = array_merge($propertyIds, $searchProperties);
767 if (empty($propertyIds))
772 $elementPropertyValues = array_fill_keys(array_keys($elements), []);
774 'IBLOCK_ID' => $iblockId,
775 'ID' => array_column($elements,
'ID'),
778 'ID' => array_unique($propertyIds),
780 \CIBlockElement::GetPropertyValuesArray($elementPropertyValues, $iblockId, $offersFilter, $propertyFilter);
782 foreach ($elementPropertyValues as $elementId => $elementProperties)
784 if (empty($elementProperties))
789 $currentElement =& $elements[$elementId];
791 foreach ($elementProperties as $property)
793 $propertyId = (int)$property[
'ID'];
795 if ($propertyId === $morePhotoId)
797 if (empty($currentElement[
'IMAGE']))
799 $propertyValue = is_array($property[
'VALUE']) ? reset($property[
'VALUE']) : $property[
'VALUE'];
800 if ((
int)$propertyValue > 0)
802 $currentElement[
'IMAGE'] = $this->getImageSource((
int)$propertyValue);
808 $isArray = is_array($property[
'~VALUE']);
810 ($isArray && !empty($property[
'~VALUE']))
811 || (!$isArray && (
string)$property[
'~VALUE'] !==
'')
814 $propertyValue = $this->getPropertyDisplayValue($property);
815 if ($propertyValue !==
'')
817 if ($skuTreeProperties !==
null && in_array($propertyId, $skuTreeProperties,
true))
819 $currentElement[
'SKU_PROPERTIES'][$propertyId] = $propertyValue;
823 $currentElement[
'SEARCH_PROPERTIES'][$propertyId] = $propertyValue;
830 $currentElement[
'SKU_PROPERTIES'] = !empty($currentElement[
'SKU_PROPERTIES'])
831 ? implode(
', ', $currentElement[
'SKU_PROPERTIES'])
834 $currentElement[
'SEARCH_PROPERTIES'] = !empty($currentElement[
'SEARCH_PROPERTIES'])
835 ? implode(
', ', $currentElement[
'SEARCH_PROPERTIES'])
839 unset($currentElement);
844 private function getPropertyDisplayValue(array $property): string
846 if (!empty($property[
'USER_TYPE']))
848 $userType = \CIBlockProperty::GetUserType($property[
'USER_TYPE']);
849 $searchMethod = $userType[
'GetSearchContent'] ??
null;
851 if ($searchMethod && is_callable($searchMethod))
853 $value = $searchMethod($property, [
'VALUE' => $property[
'~VALUE']], []);
862 $value = $property[
'~VALUE'] ??
'';
865 if (is_array($value))
867 $value = implode(
', ', $value);
870 return trim((
string)$value);
873 private function getMorePhotoPropertyId(
int $iblockId): ?int
875 $iterator = PropertyTable::getList([
878 '=IBLOCK_ID' => $iblockId,
879 '=CODE' => \CIBlockPropertyTools::CODE_MORE_PHOTO,
883 if ($row = $iterator->fetch())
885 return (
int)$row[
'ID'];
891 private function getSearchPropertyIds(
int $iblockId): array
893 $properties = PropertyTable::getList([
896 '=IBLOCK_ID' => $iblockId,
898 '=SEARCHABLE' =>
'Y',
899 '@PROPERTY_TYPE' => [
900 PropertyTable::TYPE_STRING,
901 PropertyTable::TYPE_NUMBER,
902 PropertyTable::TYPE_LIST,
905 'order' => [
'SORT' =>
'ASC',
'ID' =>
'ASC'],
909 return array_map(
'intval', array_column($properties,
'ID'));
914 if (count($products) >= self::PRODUCT_LIMIT)
919 foreach ($products as $product)
921 if (!empty($product[
'PARENT_ID']))