На сайте интернет-магазина с большим количеством товаров (от 8000) возникает проблема с формированием XML-фида для Яндекс Маркета – товаров в БД так много, что лимитов хостинга по выделяемой памяти и времени не хватает для формирования файла.
Решение проблемы – разбить генерацию файла на несколько этапов и запаковать готовый файл в ZIP-архив.
О работе скрипта
При первом запуске скрипта cron.php создается файл market_status.json для хранения необходимых переменных, формируется «шапка» файла с данными о магазине и списком категорий, далее эти данные сохраняются в файл market.xml.
В последующих запусках скрипта из базы данных берутся по 1000 товаров и дописываются в конец файла market.xml. В последнем запуске цикла, в файл дописываются закрывающие теги и происходит архивация файла в market.zip.
<?php
// Локаль.
setlocale(LC_ALL, 'ru_RU.utf8');
date_default_timezone_set('Europe/Moscow');
header('Content-type: text/html; charset=utf-8');
// Интервал обновления файла market.zip (сек.)
$interval = 12000; // Обновление раз в 3,5 часа
// Количество товаров в одном шаге
$limit = 1000;
// Подключение к БД
$dbh = new PDO('mysql:dbname=db_name;host=localhost', 'логин', 'пароль');
// Загрузка файла market_status.json
if (is_file(__DIR__ . '/market_status.json')) {
$status = json_decode(file_get_contents(__DIR__ . '/market_status.json'), true);
} else {
$status = array(
'date' => 0,
'total' => 0,
'step' => 0
);
}
if ($status['total'] == $status['step'] && $status['date'] + $interval < time()) {
// Шаг 0 - создаем новый файл market.xml
$out = '<?xml version="1.0" encoding="UTF-8"?>
<yml_catalog date="' . date('Y-m-d H:i') . '">
<shop>
<name>Название магазина</name>
<company>Полное наименование компании</company>
<url>https://example.com</url>
<currencies>
<currency id="RUR" rate="1"/>
</currencies>';
// Список категорий магазина:
$sth = $dbh->prepare("SELECT `id`, `parent`, `name` FROM `category`");
$sth->execute();
$category = $sth->fetchAll(PDO::FETCH_ASSOC);
$out .= '<categories>' . PHP_EOL;
foreach ($category as $row) {
$out .= '<category id="' . $row['id'] . '" parentId="' . $row['parent'] . '">' . $row['name'] . '</category>' . PHP_EOL;
}
$out .= '</categories>' . PHP_EOL;
$out .= '<offers>' . PHP_EOL;
// Сохраняем в файл
file_put_contents(__DIR__ . '/market.xml', $out);
// Получаем общее кол-во товаров
$sth = $dbh->prepare("SELECT COUNT(`id`) FROM `prods`");
$sth->execute();
$total = $sth->fetch(PDO::FETCH_COLUMN);
$status = array(
'date' => time(),
'total' => ceil($total / $limit),
'step' => 0
);
// Обновляем данные в market_status.json
file_put_contents(__DIR__ . '/market_status.json', json_encode($status));
echo 'Шаг ' . $status['step'] . ' из ' . $status['total'];
} elseif ($status['total'] != $status['step']) {
// Выполнение остальных шагов
$start = $status['step'] * $limit;
$sth = $dbh->prepare("SELECT * FROM `prods` LIMIT {$start}, {$limit}");
$sth->execute();
$prods = $sth->fetchAll(PDO::FETCH_ASSOC);
$out = '';
foreach ($prods as $row) {
$out .= '
<offer id="'.$row['id'] . '" available="true">
<name>' . $row['name'] . '</name>
<url>https://example.com/prods/' . $row['id'] . '</url>
<vendor>' . $row['brand'] . '</vendor>
<price>' . $row['price'] . '</price>
<currencyId>RUR</currencyId>
<categoryId>' . $row['category'] . '</categoryId>
<picture>https://example.com/uploads/' . $row['image'] . '</picture>
<description><![CDATA[' . $row['text'] . ']]></description>
</offer>';
}
// Обновляем данные в market_status.json
$status['step']++;
file_put_contents(__DIR__ . '/market_status.json', json_encode($status));
// Если последний шаг, то нужно закрыть теги
if ($status['total'] == $status['step']) {
$out .= '</offers></shop></yml_catalog>';
}
// Сохраняем данные в market.xml
$fp = fopen(__DIR__ . '/market.xml', 'a');
fwrite($fp, PHP_EOL . $out);
fclose($fp);
// Если последний шаг, то ZIP-архивация
if ($status['total'] == $status['step']) {
$zip = new ZipArchive();
$zip->open(__DIR__ . '/market.zip', ZipArchive::CREATE|ZipArchive::OVERWRITE);
$zip->addFile(__DIR__ . '/market.xml', 'market.xml');
$zip->close();
}
echo 'Шаг ' . $status['step'] . ' из ' . $status['total'];
} else {
// Файл недавно сформирован, пока отдыхаем
echo 'Файл сформирован, следующее обновление в ' . date('d.m.Y H:s', $status['date'] + $interval);
}
Данный скрип должен вызываться поочередно, например по крону c интервалом раз в 15 минут:
Минута Час День Месяц День недели Команда
*/15 * * * * /usr/local/bin/wget -O - -q "https://example.com/cron.php"