Обновление свойства заказа по городу пользователя (инфоблок, Sale D7)
Задача
Нужно периодически пройти по заказам и заполнить свойство оформления (в исходной заметке — «страна») по связке пользователь заказа → город в справочнике инфоблока → верхний уровень дерева разделов. Типичный сценарий: справочник городов лежит в инфоблоке с иерархией «страна → регион → город», у пользователя в поле UF_CITY хранится элемент города.
Ограничения и безопасность
- Запускайте только из CLI (
php script.phpили cron), чтобы случайный веб-запрос не включил массовую перезапись. - Сначала проверка на копии БД и на узком фильтре по датам или ID заказов.
- Константа
COUNTRY_IBLOCK_IDи ID свойства заказа должны быть заменены на ваши значения из административки.
Современная схема на D7
Список заказов без legacy CSaleOrder удобнее строить через OrderTable, объект заказа — \Bitrix\Sale\Order::load(). Значение свойства меняется через коллекцию getPropertyCollection(); если строки значения для данного свойства ещё нет, создаём элемент методом createItem() с ключом ORDER_PROPS_ID и сохраняем заказ.
if (PHP_SAPI !== 'cli') {
fwrite(STDERR, "CLI only\n");
exit(1);
}
$_SERVER['DOCUMENT_ROOT'] = realpath(__DIR__ . '/..');
define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
use Bitrix\Main\Loader;
use Bitrix\Sale\Order;
use Bitrix\Sale\Internals\OrderTable;
Loader::includeModule('sale');
Loader::includeModule('iblock');
if (!defined('COUNTRY_IBLOCK_ID')) {
throw new RuntimeException('Define COUNTRY_IBLOCK_ID');
}
final class OrdersCountrySync
{
private int $countryPropertyId;
/** корневой раздел ID => ожидаемое имя страны (контроль по белому списку) */
private static array $countries = [
26 => 'Россия',
8 => 'Казахстан',
1 => 'Беларусь',
105 => 'Украина',
];
public function __construct(int $countryOrderPropertyId)
{
$this->countryPropertyId = $countryOrderPropertyId;
}
public function run(): void
{
$cursor = OrderTable::getList([
'select' => ['ID'],
'order' => ['ID' => 'DESC'],
]);
while ($row = $cursor->fetch()) {
$orderId = (int) $row['ID'];
try {
$this->syncOrder($orderId);
} catch (Throwable $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
}
echo 'Done: ' . $orderId . PHP_EOL;
}
}
private function syncOrder(int $orderId): void
{
$orderEntity = Order::load($orderId);
$userId = (int) $orderEntity->getUserId();
$userRow = \CUser::GetByID($userId)->Fetch();
if (!$userRow) {
throw new RuntimeException('No user ' . $userId . ' for order ' . $orderId);
}
$cityElementId = (int) ($userRow['UF_CITY'] ?? 0);
$countryName = $this->resolveCountryName($cityElementId);
$props = $orderEntity->getPropertyCollection();
$cell = $props->getItemByOrderPropertyId($this->countryPropertyId);
if (!$cell) {
$cell = $props->createItem([
'ORDER_PROPS_ID' => $this->countryPropertyId,
]);
}
$cell->setValue($countryName);
$save = $orderEntity->save();
if (!$save->isSuccess()) {
throw new RuntimeException(
'Order ' . $orderId . ': ' . implode('; ', $save->getErrorMessages())
);
}
}
private function resolveCountryName(int $cityElementId): string
{
if ($cityElementId <= 0) {
throw new RuntimeException('Empty UF_CITY');
}
$rs = \CIBlockElement::GetList(
[],
[
'IBLOCK_ID' => COUNTRY_IBLOCK_ID,
'ID' => $cityElementId,
'CHECK_PERMISSIONS' => 'N',
'MIN_PERMISSION' => 'R',
],
false,
false,
['ID', 'IBLOCK_SECTION_ID', 'NAME']
);
$location = $rs->Fetch();
if (!$location) {
throw new RuntimeException('No location element ' . $cityElementId);
}
$nav = \CIBlockSection::GetNavChain(
COUNTRY_IBLOCK_ID,
(int) $location['IBLOCK_SECTION_ID']
);
$top = $nav->Fetch();
if (!$top) {
throw new RuntimeException('No section chain for element ' . $cityElementId);
}
$topId = (int) $top['ID'];
if (!array_key_exists($topId, self::$countries)) {
throw new RuntimeException('Unknown country section ID ' . $topId);
}
return (string) $top['NAME'];
}
}
($sync = new OrdersCountrySync(10))->run();Замечания по логике
- Массив
$countriesв примере дублирует проверку из оригинала: в него попадают только «разрешённые» ID корневых разделов. Если справочник стабилен, допустимо убрать белый список и брать имя верхнего раздела как есть. GetNavChainвозвращает цепочку от корня к текущему разделу; первыйFetch()даёт корень — для схемы «страна сверху» этого достаточно.- Чтобы не грузить тысячи заказов за один проход, добавьте
'filter'по диапазонуDATE_INSERTили обрабатывайте только заказы без заполненного свойства.
Связь со старым API
Скрипт с CSaleOrder, CSaleOrderPropsValue::Update/Add и тем же деревом городов эквивалентен по смыслу: переписанный вариант лишь выносит обход заказов и сохранение в API модуля Sale D7.
Не хотите копаться сами?
Починю за 1-3 дня. Без предоплаты — оплата по результату.
15+ лет опыта с 1С-Битрикс · Без предоплаты · 7 дней гарантии