Существует 2 варианта представления каталога товаров на сайте: простой - когда товар и все его торговые предложения имеют один и тот же url-адрес (и частный случай - каталог без торговых предложений) и более сложный - когда торговые предложения имеют собственные url-адреса, отличающиеся от основного товара какой-то дополнительной солью.
Для первого варианта, нужно обрабатывать именно каталог, а цены и остатки искать в торговых предложениях. Для второго - обрабатывать нужно торговые предложения, а недостающие данные брать из товара.
В данном посте рассмотрю простой вариант, т.к. он встречается чаще (в моей работе), хотя с точки зрения сео - вариант отдельных адресов может быть и не совсем верно.
В любом случае, принимать решение о том, какой вариант лучше - вам, причем, в каждой конкретной ситуации.
Для начала кладем где-то файл с классом, например сюда: /bitrix/php_interface/classes/ccatalogexport.php следующего содержания:
namespace Pai\Tools;
use \Bitrix\Main\Data\Cache;
use Bitrix\Main\IO,
Bitrix\Main\Application;
use Bitrix\Main\Loader;
class CCatalogExport
{
var $CATALOG_IBLOCK;
var $OFFER_IBLOCK;
var $EXCLUDE_SECTIONS = [83, 88, 89]; // тут указываем разделы, которые выгружать не нужно
var $FirmName = 'ТМ Торговая Марка';
var $url = "https://site.ua";
var $Scheme;
var $result;
function __construct($CATALOG_IBLOCK, $OFFER_IBLOCK)
{
$this->CATALOG_IBLOCK = $CATALOG_IBLOCK;
$this->OFFER_IBLOCK = $OFFER_IBLOCK;
}
private function GetSectionsList()
{
$result = false;
$needNavChain = false;
$cache_params = array('function' => __FUNCTION__, 'IBLOCK_ID' => $this->CATALOG_IBLOCK,
'EXCLUDE' => $this->EXCLUDE_SECTIONS, $needNavChain);
$cache_id = md5(serialize($cache_params));
$cache_dir = __CLASS__;
$cache = Cache::createInstance();
$life_time = 60 * 60 * 24 * 30;
if ($life_time < 0)
{
$cache->clean($cache_id, $cache_dir);
}
if ($cache->initCache($life_time, $cache_id, $cache_dir))
{
$result = $cache->getVars();
} elseif ($cache->startDataCache() && \Bitrix\Main\Loader::includeModule('iblock'))
{
$Sect_list = \CIBlockSection::GetList(array("DEPTH_LEVEL" => "ASC", "ID" => "ASC"),
array('IBLOCK_ID' => $this->CATALOG_IBLOCK, 'GLOBAL_ACTIVE' => 'Y', '!ID' => $this->EXCLUDE_SECTIONS),
false, array('ID', 'IBLOCK_ID', 'NAME', 'SECTION_PAGE_URL', 'IBLOCK_SECTION_ID'));
while ($Section = $Sect_list->GetNext())
{
if ($needNavChain)
{
$nav = \CIBlockSection::GetNavChain(false, $Section['ID']);
while ($arSectionPath = $nav->GetNext())
{
$Section['SECTIONS_PATH'][$arSectionPath['DEPTH_LEVEL']] = $arSectionPath;
}
}
$result[$Section['ID']] = $Section;
}
$cache->endDataCache($result);
}
$this->result['SECTIONS'] = $result;
}
private function GetIblockElements()
{
$result = false;
$cache_params = array('function' => __FUNCTION__, 'IBLOCK_ID' => $this->CATALOG_IBLOCK,
'EXCLUDE' => $this->EXCLUDE_SECTIONS);
$cache_id = md5(serialize($cache_params));
$cache_dir = __CLASS__;
$cache = Cache::createInstance();
$life_time = 60 * 60 * 24;
if ($life_time < 0)
{
$cache->clean($cache_id, $cache_dir);
}
if ($cache->initCache($life_time, $cache_id, $cache_dir))
{
$result = $cache->getVars();
} elseif ($cache->startDataCache() && \Bitrix\Main\Loader::includeModule('iblock'))
{
$arSelect = Array("ID", "NAME", "IBLOCK_ID", "CATALOG_TYPE", "IBLOCK_SECTION_ID", "DETAIL_PAGE_URL",
"DETAIL_PICTURE", "PREVIEW_PICTURE", "DETAIL_TEXT");
$arFilter = Array("IBLOCK_ID" => $this->CATALOG_IBLOCK, "ACTIVE_DATE" => "Y", "ACTIVE" => "Y",
'!IBLOCK_SECTION_ID' => $this->EXCLUDE_SECTIONS, "CATALOG_TYPE" => array(1, 3));
$res = \CIBlockElement::GetList(Array(), $arFilter, false, false, $arSelect);
while ($ob = $res->GetNextElement())
{
$arFields = $ob->GetFields();
/*Loader::includeModule('pai.tools');
CDebug::pre($arFields,true);*/
$arFields['CATALOG_QUANTITY'] = intval($arFields['CATALOG_QUANTITY']);
$price = 0;
if (intval($arFields['DETAIL_PICTURE']) <= 0 && intval($arFields['PREVIEW_PICTURE']) > 0)
{
$arFields['DETAIL_PICTURE'] = $arFields['PREVIEW_PICTURE'];
}
if ($arFields['CATALOG_TYPE'] == 3)
{
// у товара есть торговые предложения - их нужно найти :(
$arOfferSelect = Array("ID", "NAME", "IBLOCK_ID", "CATALOG_QUANTITY");
if (intval($arFields['DETAIL_PICTURE']) <= 0 && intval($arFields['PREVIEW_PICTURE']) <= 0)
{
$arOfferSelect[] = 'DETAIL_PICTURE';
$arOfferSelect[] = 'PREVIEW_PICTURE';
}
$arOfferFilter = Array("IBLOCK_ID" => $this->OFFER_IBLOCK, "PROPERTY_CML2_LINK" => $arFields['ID'],
"ACTIVE_DATE" => "Y", "ACTIVE" => "Y");
$resOffer = \CIBlockElement::GetList(Array(), $arOfferFilter, false, false, $arOfferSelect);
while ($arOfferFields = $resOffer->GetNext())
{
$dbProductPrice = \CPrice::GetListEx(
array(),
array("PRODUCT_ID" => $arOfferFields['ID'], "CATALOG_GROUP_ID" => 1),
// берем товары с базовой ценой, с ID=1
false,
false,
array("ID", "CATALOG_GROUP_ID", "PRICE", "CURRENCY")
);
while ($arPrice = $dbProductPrice->Fetch())
{
if (!isset($arFields['PRICE']) || floatval($arFields['PRICE']) > floatval($arPrice['PRICE']))
{
$arFields['PRICE'] = floatval($arPrice['PRICE']);
$arFields['CURRENCY'] = $arPrice['CURRENCY'];
}
}
$arFields['CATALOG_QUANTITY'] += intval($arOfferFields['CATALOG_QUANTITY']);
if (intval($arFields['DETAIL_PICTURE']) <= 0 && (intval($arOfferFields['DETAIL_PICTURE']) > 0
|| intval($arOfferFields['PREVIEW_PICTURE']) > 0))
{
$arFields['DETAIL_PICTURE'] = (intval($arOfferFields['DETAIL_PICTURE']) > 0)
? $arOfferFields['DETAIL_PICTURE']
: $arOfferFields['PREVIEW_PICTURE'];
}
}
} else
{
// простой товар
$dbProductPrice = \CPrice::GetListEx(
array(),
array("PRODUCT_ID" => $arFields['ID'], "CATALOG_GROUP_ID" => 1),
// берем товары с базовой ценой, с ID=1
false,
false,
array("ID", "CATALOG_GROUP_ID", "PRICE", "CURRENCY")
);
while ($arPrice = $dbProductPrice->Fetch())
{
if (!isset($arFields['PRICE']) || floatval($arFields['PRICE']) > floatval($arPrice['PRICE']))
{
$arFields['PRICE'] = floatval($arPrice['PRICE']);
$arFields['CURRENCY'] = $arPrice['CURRENCY'];
}
}
}
if (floatval($arFields['PRICE']) <= 0) continue;
if (intval($arFields['DETAIL_PICTURE']) <= 0) continue;
$arProperties = $ob->GetProperties();
$arDisplay_properties = [];
$MorePictures = [];
foreach ($arProperties as $arProperty)
{
if($arProperty['CODE']=='MORE_PHOTO' && !empty($arProperty['VALUE'])){
foreach ($arProperty['VALUE'] as $fid){
$MorePictures[] = \CFile::GetPath($fid);
}
}
if (in_array($arProperty['CODE'], array(
'MINIMUM_PRICE',
'MAXIMUM_PRICE',
'HIT',
'POPUP_VIDEO',
'PODBORKI',
'PRODUCTION',
'COLOR',
'SPECIAL_OFFERS',
'META_DESCRIPTION',
'META_KEYWORDS',
'MORE_PHOTO', // ???
'COLOR_REF2',
'SALE_TEXT',
'vote_count',
'vote_sum',
'rating',
'FORUM_TOPIC_ID',
'FORUM_MESSAGE_CNT',
'COLOR_VARIANTS',
'PRODUCT_TYPE',
'VIDEO_YOUTUBE',
'EXPANDABLES',
'ASSOCIATED',
))) continue;
$arDisplay_properties[$arProperty['CODE']] = \CIBlockFormatProperties::GetDisplayValue(
array('ID' => $arFields['ID'], 'NAME' => $arFields['NAME']), $arProperty, '');
}
$row = array(
'id' => $arFields['ID'],
'available' => (intval($arFields['CATALOG_QUANTITY']) > 0) ? "true" : "false",
'categoryId' => $arFields['IBLOCK_SECTION_ID'],
'vendor' => 'Brand name',
'name' => $arFields['NAME'],
'url' => $arFields['DETAIL_PAGE_URL'],
'price' => $arFields['PRICE'],
'stock' => (intval($arFields['CATALOG_QUANTITY']) > 0) ? 'На складе' : "Нет на складе",
'params' => [],
'picture' => \CFile::GetPath($arFields['DETAIL_PICTURE']),
'more_pictures' => $MorePictures,
'description' => ((strlen($arFields['DETAIL_TEXT']) > 0) ?
'' . PHP_EOL : "")
);
foreach ($arDisplay_properties as $arProperty)
{
if (is_array($arProperty['DISPLAY_VALUE'])) $arProperty['DISPLAY_VALUE'] =
implode(', ', $arProperty['DISPLAY_VALUE']);
if (strlen($arProperty['DISPLAY_VALUE']) > 0)
{
$row['params'][$arProperty['NAME']] = $arProperty['DISPLAY_VALUE'];
}
}
$result[$arFields['ID']] = $row;
}
$cache->endDataCache($result);
}
$this->result['PRODUCTS'] = $result;
}
function GetFileSheme()
{
$Scheme = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" . PHP_EOL;
$Scheme .= "<price>" . PHP_EOL;
$Scheme .= "\t<date>" . date(str_replace("_", " ", '"Y-m-dTh:i:s±h:i"')) . "</date>" . PHP_EOL;
$Scheme .= "\t<firmName>" . $this->FirmName . "</firmName>" . PHP_EOL;
$Scheme .= "\t<rate>1</rate>" . PHP_EOL;
$Scheme .= "\t<url>" . $this->url . "</url>" . PHP_EOL;
$Scheme .= "\t<url>" . $this->url . "</url>" . PHP_EOL;
$Scheme .= "\t<categories>" . PHP_EOL;
$Scheme .= "#CATEGORIES#" . PHP_EOL;
$Scheme .= "\t</categories>" . PHP_EOL;
$Scheme .= "\t<offers>" . PHP_EOL;
$Scheme .= "#PRODUCTS#" . PHP_EOL;
$Scheme .= "\t</offers>" . PHP_EOL;
$Scheme .= "</price>";
$this->Scheme = $Scheme;
}
function putSectionsToScheme()
{
$categories = '';
foreach ($this->result['SECTIONS'] as $arSection)
{
$categories .= "\t\t<category id=\"" . $arSection['ID'] . "\" " .
(
(intval($arSection['IBLOCK_SECTION_ID']) > 0)
? "parentId=\"" . $arSection['IBLOCK_SECTION_ID'] . "\" "
: "")
. "portal_url=\"" . $arSection['SECTION_PAGE_URL'] . "\">" . $arSection["NAME"] . "</category>" . PHP_EOL;
}
$this->Scheme = str_replace('#CATEGORIES#', TrimEx($categories, PHP_EOL, 'right'), $this->Scheme);
}
function putProductsToScheme()
{
$productsLine = '';
foreach ($this->result['PRODUCTS'] as $arProduct)
{
$productsLine .= "\t\t<offer id=\"" . $arProduct["id"] . "\" available=\""
. $arProduct["available"] . "\">" . PHP_EOL;
$productsLine .= "\t\t\t<categoryId>" . $arProduct['categoryId'] . "</categoryId>" . PHP_EOL;
$productsLine .= "\t\t\t<vendor>" . $arProduct['vendor'] . "</vendor>" . PHP_EOL;
$productsLine .= "\t\t\t<name>" . $arProduct['name'] . "</name>" . PHP_EOL;
$productsLine .= "\t\t\t<url>" . $this->url . $arProduct['url'] . "</url>" . PHP_EOL;
$productsLine .= "\t\t\t<price>" . $arProduct['price'] . "</price>" . PHP_EOL;
$productsLine .= "\t\t\t<stock>" . $arProduct['stock'] . "</stock>" . PHP_EOL;
$productsLine .= "\t\t\t<picture>" . $this->url . $arProduct['picture'] . "</picture>" . PHP_EOL;
$productsLine .= "\t\t\t<description>" . $arProduct['description'] . PHP_EOL . "</description>" . PHP_EOL;
foreach ($arProduct['params'] as $param => $paramValue)
{
$productsLine .= "\t\t\t<param name=\"" . $param . "\">" . $paramValue . "</param>" . PHP_EOL;
}
$productsLine .= "\t\t</offer>" . PHP_EOL;
}
$this->Scheme = str_replace('#PRODUCTS#', TrimEx($productsLine, PHP_EOL, 'right'), $this->Scheme);
}
function putSchemeToFile()
{
$file = new IO\File(Application::getDocumentRoot() . "/upload/export/tmp.xml");
$file->putContents($this->Scheme);
$file->rename(Application::getDocumentRoot() . "/upload/export/siteCatalog.xml");
echo 'Ok';
}
function process()
{
$this->GetFileSheme();
$this->GetSectionsList();
$this->putSectionsToScheme();
$this->GetIblockElements();
$this->putProductsToScheme();
$this->putSchemeToFile();
}
static function Agent($IBLOCK_ID, $OFFERS_ID)
{
$processor = new CCatalogExport($IBLOCK_ID, $OFFERS_ID);
$processor->process();
return "\Pai\Tools\CCatalogExport::Agent($IBLOCK_ID,$OFFERS_ID);";
}
}
Дальше, подключаем данный класс к автозагрузке:
Bitrix\Main\Loader::registerAutoLoadClasses(null, array( 'Pai\Tools\CCatalogExport' => '/bitrix/php_interface/classes/ccatalogexport.php', ));
Проверяем работу класса запустив скрипт:
$processor = new Pai\Tools\CCatalogExport(, ); $processor->process();
И последним шагом - создаем периодический агент с выполнением функции:
\Pai\Tools\CCatalogExport::Agent($IBLOCK_ID,$OFFERS_ID);
Имейте ввиду, что данный вариант скрипта не подходит для больших каталогов, которые нельзя обработать за один хит. Если у вас большой каталог - стоит разбить его на части с помощью пагинации и информацию о товарах записывать в файл пошагово.
2018-12-27. Создание дополнительной выгрузки на базе основной.
Предположим, вы создали и настроили основную выгрузку, но вам нужна еще одна (а в реальной ситуации - не одна) с незначительными изменениями структуры. Для этого, нужно создать еще один класс с наследованинем от текущего.
Итак, создаем рядом с файлом с основным классом ее один, например, класс CYmlCatalogExport в файле cymlcatalogexport. В init.php добавляем автозагрузку нового класса:
Bitrix\Main\Loader::registerAutoLoadClasses(null, array( 'Pai\Tools\CCatalogExport' => '/bitrix/php_interface/classes/ccatalogexport.php', 'Pai\Tools\CYmlCatalogExport' => '/bitrix/php_interface/classes/cymlcatalogexport.php', ));
Сам класс примет такой вид:
namespace Pai\Tools;
use \Bitrix\Main\Data\Cache;
use Bitrix\Main\IO,
Bitrix\Main\Application;
use Bitrix\Main\Loader;
class CYmlCatalogExport extends CCatalogExport
{
function GetFileSheme()
{
$Scheme = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" . PHP_EOL;
$Scheme .= "<!DOCTYPE yml_catalog SYSTEM \"shops.dtd\" >".PHP_EOL;
$Scheme .= "<yml_catalog date=\"".date('Y-m-d H:i')."\">" . PHP_EOL;
$Scheme .= "<shop>".PHP_EOL;
$Scheme .= "\t<name>" . $this->FirmName . "</name>" . PHP_EOL;
$Scheme .= "\t<company>" . $this->FirmName . "</company>" . PHP_EOL;
$Scheme .= "\t<url>" . $this->url . "</url>" . PHP_EOL;
$Scheme .= "\t<platform>" . "1С-Битрикс" . "</platform>" . PHP_EOL;
$Scheme .= "\t<currencies>".PHP_EOL;
$Scheme .= "\t\t<currency id=\"RUR\" rate=\"1.0\" />".PHP_EOL;
$Scheme .= "\t</currencies>".PHP_EOL;
$Scheme .= "\t<categories>" . PHP_EOL;
$Scheme .= "#CATEGORIES#" . PHP_EOL;
$Scheme .= "\t</categories>" . PHP_EOL;
$Scheme .= "\t<offers>" . PHP_EOL;
$Scheme .= "#PRODUCTS#" . PHP_EOL;
$Scheme .= "\t</offers>" . PHP_EOL;
$Scheme .= "</shop>".PHP_EOL;
$Scheme .= "</yml_catalog>";
$this->Scheme = $Scheme;
}
function putSectionsToScheme()
{
$categories = '';
foreach ($this->result['SECTIONS'] as $arSection)
{
$categories .= "\t\t<category id=\"" . $arSection['ID'] . "\" " .
(
(intval($arSection['IBLOCK_SECTION_ID']) > 0)
? "parentId=\"" . $arSection['IBLOCK_SECTION_ID'] . "\" "
: "")
. "portal_url=\"" . $arSection['SECTION_PAGE_URL'] . "\">" . $arSection["NAME"] . "</category>" . PHP_EOL;
}
$this->Scheme = str_replace('#CATEGORIES#', TrimEx($categories, PHP_EOL, 'right'), $this->Scheme);
}
function putProductsToScheme()
{
$productsLine = '';
foreach ($this->result['PRODUCTS'] as $arProduct)
{
$productsLine .= "\t\t<offer id=\"" . $arProduct["id"] . "\" available=\""
. $arProduct["available"] . "\">" . PHP_EOL;
$productsLine .= "\t\t\t<categoryId>" . $arProduct['categoryId'] . "</categoryId>" . PHP_EOL;
$productsLine .= "\t\t\t<currencyId>" . $arProduct['currencyId'] . "</currencyId>" . PHP_EOL;
$productsLine .= "\t\t\t<name>" . htmlspecialchars($arProduct['name']) . "</name>" . PHP_EOL;
$productsLine .= "\t\t\t<url>" . $this->url . $arProduct['url'] . "</url>" . PHP_EOL;
$productsLine .= "\t\t\t<price>" . $arProduct['price'] . "</price>" . PHP_EOL;
$productsLine .= "\t\t\t<picture>" . $this->url . $arProduct['picture'] . "</picture>" . PHP_EOL;
if(!empty($arProduct['more_pictures'])){
$productsLine .= "\t\t\t<add_picture>" . implode(',',$arProduct['more_pictures'])."</add_picture>".PHP_EOL;
}
$productsLine .= "\t\t\t<description>" . $arProduct['description'] . PHP_EOL . "</description>" . PHP_EOL;
$productsLine .= "\t\t\t<store>false</store>".PHP_EOL;
$productsLine .= "\t\t\t<pickup>true</pickup>".PHP_EOL;
$productsLine .= "\t\t\t<delivery>true</delivery>".PHP_EOL;
foreach ($arProduct['params'] as $param => $paramValue)
{
$productsLine .= "\t\t\t<param name=\"" . $param . "\">" . $paramValue . "</param>" . PHP_EOL;
}
$productsLine .= "\t\t</offer>" . PHP_EOL;
}
$this->Scheme = str_replace('#PRODUCTS#', TrimEx($productsLine, PHP_EOL, 'right'), $this->Scheme);
}
function putSchemeToFile()
{
$file = new IO\File(Application::getDocumentRoot() . "/upload/export/tmp.xml");
$file->putContents($this->Scheme);
$file->rename(Application::getDocumentRoot() . "/upload/export/ymlCatalog.xml");
echo 'Ok';
}
static function Agent($IBLOCK_ID, $OFFERS_ID)
{
$processor = new CYmlCatalogExport($IBLOCK_ID, $OFFERS_ID);
$processor->process();
return "\Pai\Tools\CYmlCatalogExport::Agent($IBLOCK_ID,$OFFERS_ID);";
}
}
Вот так получаем вторую выгрузку на базе первой.