Блог разработчика 1С-Битрикс

Автозаполнение местоположения пользователя в bitrix:sale.order.ajax: определяем город и подставляем его в заказ

В интернет-магазине каждый лишний клик снижает конверсию. Один из самых раздражающих моментов — выбор города в форме оформления заказа. В Bitrix bitrix:sale.order.ajax это свойство типа LOCATION. В статье показываю, как автоматически подставлять местоположение покупателя в это поле: сначала — полный рабочий код (он ниже), затем разберём логику, тонкости и еще один более надёжный, «битриксовый» апгрейд.

Автозаполнение местоположения пользователя в bitrix:sale.order.ajax
<?php
// /local/php_interface/init.php
\Bitrix\Main\EventManager::getInstance()->addEventHandlerCompatible(
    'sale',
    'OnSaleComponentOrderProperties',
    [SaleOrderEvents::class, 'fillLocation']
);
class SaleOrderEvents {
    public static function fillLocation(&$arUserResult, $request, &$arParams, &$arResult)
    {
        $registry = \Bitrix\Sale\Registry::getInstance(\Bitrix\Sale\Registry::REGISTRY_TYPE_ORDER);
        $orderClassName = $registry->getOrderClassName();
        $order = $orderClassName::create(\Bitrix\Main\Application::getInstance()->getContext()->getSite());
        $propertyCollection = $order->getPropertyCollection();
        foreach ($propertyCollection as $property) {
            if ($property->isUtil()) {
                continue;
            }
            $arProperty = $property->getProperty();
            if (
                $arProperty['TYPE'] === 'LOCATION'
                && array_key_exists($arProperty['ID'], $arUserResult["ORDER_PROP"])
                && !$request->getPost("ORDER_PROP_" . $arProperty['ID'])
                && (
                    !is_array($arOrder = $request->getPost("order"))
                    || !$arOrder["ORDER_PROP_" . $arProperty['ID']]
                )
            ) {
                // Определяем местоположение пользователя
                $userCityCode = self::getUserCityCode();
                if ($userCityCode) {
                    $arUserResult["ORDER_PROP"][$arProperty['ID']] = $userCityCode;
                }
            }
        }
    }
    private static function getUserCityCode()
    {
        // 1. Попробуем получить город из профиля пользователя (если авторизован)
        global $USER;
        if ($USER->IsAuthorized()) {
            $userID = $USER->GetID();
            $user = new CUser;
            $userData = $user->GetByID($userID)->Fetch();
            if (!empty($userData['PERSONAL_CITY'])) {
                $location = CSaleLocation::GetByID($userData['PERSONAL_CITY']);
                if ($location) {
                    return $location['CODE'];
                }
            }
        }
        // 2. Попробуем определить город по IP
        $ip = $_SERVER['REMOTE_ADDR'];
        $cityData = self::getCityByIp($ip);
        if ($cityData && !empty($cityData['city'])) {
            // Ищем местоположение в базе Битрикс по названию города
            $filter = array(
                'LID' => LANGUAGE_ID,
                'CITY_NAME' => $cityData['city']
            );
            $location = CSaleLocation::GetList(array(), $filter)->Fetch();
            if ($location) {
                return $location['CODE'];
            }
        }
        // 3. Можно добавить fallback на город по умолчанию
        return false;
    }
    private static function getCityByIp($ip)
    {
        // Используем сервис для определения города по IP
        // Например, можно использовать http://ip-api.com/json/
        $url = "http://ip-api.com/json/{$ip}";
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 3);
        $response = curl_exec($ch);
        curl_close($ch);
        return json_decode($response, true);
    }
}
?>

Что делает код

Код подписывается на событие OnSaleComponentOrderProperties модуля sale и, когда форма заказа только собирается, пытается определить город пользователя:

  1. Проверяет, не ввёл ли пользователь LOCATION сам. Если поле уже заполнено (есть POST), автоподстановка не сработает — мы не переписываем то, что ввёл человек.
  2. Пытается достать город из профиля (если пользователь авторизован).
  3. Если не получилось — определяет город по IP через внешний GeoIP-сервис и ищет соответствующий элемент в справочнике местоположений Битрикс.
  4. Нашёл — записывает CODE города в $arUserResult["ORDER_PROP"][ID]. Именно CODE нужен для свойства типа LOCATION.

Как это работает пошагово

1) Регистрация обработчика события

EventManager::getInstance()->addEventHandlerCompatible(
  'sale',
  'OnSaleComponentOrderProperties',
  [SaleOrderEvents::class, 'fillLocation']
);

Когда компонент bitrix:sale.order.ajax подготавливает набор свойств заказа, он кидает событие. Наш обработчик получает ссылки на $arUserResult (итог для формы), $arParams, $arResult и объект $request.

2) Получаем коллекцию свойств заказа

$registry = \Bitrix\Sale\Registry::getInstance(\Bitrix\Sale\Registry::REGISTRY_TYPE_ORDER);
$orderClassName = $registry->getOrderClassName();
$order = $orderClassName::create(\Bitrix\Main\Application::getInstance()->getContext()->getSite());
$propertyCollection = $order->getPropertyCollection();

Создаём «пустой» заказ и его PropertyCollection. Это нужно, чтобы корректно обойти свойства, узнать их типы и ID.

3) Фильтруем и находим LOCATION

foreach ($propertyCollection as $property) {
    if ($property->isUtil()) continue; // служебные пропы не трогаем
    $arProperty = $property->getProperty();
    if (
        $arProperty['TYPE'] === 'LOCATION'
        && array_key_exists($arProperty['ID'], $arUserResult["ORDER_PROP"])
        && !$request->getPost("ORDER_PROP_" . $arProperty['ID'])
        && (!is_array($arOrder = $request->getPost("order")) || !$arOrder["ORDER_PROP_" . $arProperty['ID']])
    ) {
        // здесь будет автоподстановка
    }
}

Условие гарантирует, что:

  • это именно поле типа LOCATION;
  • оно присутствует в результирующем наборе;
  • пользователь не прислал значение сам (ни в прямом POST, ни во вложенном order[...]).

4) Определяем город (getUserCityCode())

Функция идёт по цепочке:

  • Из профиля: если пользователь залогинен и в профиле заполнен город, пытаемся его использовать.
  • По IP: берём IP клиента, вызываем внешний API (ip-api.com) → получаем название города → ищем его в базе местоположений Битрикс и берём CODE.
  • Иначе: возвращаем false (ничего не подставляем).

5) Подставляем значение

Если получили CODE — записываем его в $arUserResult["ORDER_PROP"][ID]. Компонент возьмёт это значение по умолчанию и отрисует селектор LOCATION уже с нужным городом.

Важные нюансы и подводные камни

  • PERSONAL_CITY — это текст, а не ID. В профиле пользователя поле «Город» (PERSONAL_CITY) обычно строковое, а не «ссылка» на справочник местоположений. В приведённом коде вызов CSaleLocation::GetByID($userData['PERSONAL_CITY']) сработает только если там действительно ID, что редко. Надёжнее искать по названию (см. улучшенный вариант ниже).
  • CODE vs ID. Свойство LOCATION в современных установках работает через CODE (символьный код местоположения), а не через числовой ID. Именно его мы и подставляем.
  • Мультиязычность. Поиск по CITY_NAME чувствителен к языку и орфографии. «Москва» ≠ «Moscow». Убедитесь, что ваш LANGUAGE_ID и база местоположений согласованы.
  • IP через прокси/CDN. $_SERVER['REMOTE_ADDR'] может принадлежать прокси, тогда GeoIP покажет «город CDN». Для продакшна лучше учитывать заголовки X-Forwarded-For/X-Real-IP (если доверяете им) или использовать встроенный GeoIP-менеджер Битрикс.
  • Зависимость от внешнего сервиса. Лимиты, задержки и падения стороннего API — риск. В коде таймаут 3 сек, но всё равно это блокирующий вызов на критическом пути. Желательно кэшировать результаты и/или перейти на локальную базу GeoIP.
  • Переопределение пользователем. После автоподстановки пользователь должен иметь возможность выбрать другой город — условие в коде это уважает.

Тест-план (чтобы не словить регресс)

  1. Гость без POST: город подставляется из GeoIP.
  2. Авторизованный, город в профиле: берётся профильный город.
  3. Пользователь вручную меняет LOCATION: наш код не перетирает его выбор.
  4. Город из GeoIP отсутствует в справочнике: поле остаётся пустым, заказ оформляется.
  5. Сайт за CDN: проверить корректность IP и что не подставляется «Лондон» вместо «Тверь»

Улучшаем код: D7 + встроенный GeoIP + корректный поиск локации

Преимущества этого подхода:

  1. Использует встроенный GeoIP Битрикс - более надежно и не требует внешних HTTP-запросов
  2. Правильное определение IP-адреса с учетом заголовков прокси
  3. Поиск по названию города в базе местоположений Битрикс
  4. Приоритетность - сначала проверяет профиль пользователя, затем GeoIP
  5. Типизация - строгая типизация метода getClientIp()

Вставьте эти методы вместо текущих getUserCityCode() и getCityByIp() и поправьте use-импорты при необходимости.

<?php
use Bitrix\Main\Service\GeoIp\Manager as GeoIpManager;
class SaleOrderEvents
{
    // ...
    private static function getUserCityCode()
    {
        // 1) Авторизованный пользователь: PERSONAL_CITY -> ищем по названию
        global $USER;
        if (is_object($USER) && $USER->IsAuthorized()) {
            $user = CUser::GetByID($USER->GetID())->Fetch();
            if (!empty($user['PERSONAL_CITY'])) {
                if ($code = self::findLocationCodeByCityName($user['PERSONAL_CITY'])) {
                    return $code;
                }
            }
        }
        // 2) Определяем город через встроенный GeoIP (без внешних HTTP)
        $ip = self::getClientIp();
        $geo = GeoIpManager::getDataResult($ip, LANGUAGE_ID);
        if ($geo && $geo->isSuccess()) {
            $data = $geo->getGeoData();
            if (!empty($data->cityName)) {
                if ($code = self::findLocationCodeByCityName($data->cityName)) {
                    return $code;
                }
            }
        }
        // 3) Fallback: верните CODE вашего дефолтного местоположения, если хотите
        // return '0000073738';
        return false;
    }
    private static function getClientIp(): string
    {
        // аккуратнее с заголовками: доверяйте им только если настроены прокси
        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            $parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
            return trim($parts[0]);
        }
        if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
            return $_SERVER['HTTP_X_REAL_IP'];
        }
        return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
    }
    private static function findLocationCodeByCityName(string $cityName)
    {
        // Быстрый и совместимый путь: старый API поиска по названию
        $loc = CSaleLocation::GetList(
            [],
            ['LID' => LANGUAGE_ID, 'CITY_NAME' => $cityName],
            false,
            false,
            ['ID', 'CODE', 'CITY_NAME']
        )->Fetch();
        return $loc['CODE'] ?? false;
    }
    // ...
}
?>

Почему так лучше

  • Нет внешних запросов на оформлении — быстрее и стабильнее.
  • PERSONAL_CITY используется по назначению (как строка), а не как ID.
  • Логика осталась прежней: по-прежнему не перезаписываем пользовательский ввод.

SEO-заметки и UX-советы

  • Скорость = конверсия. Благодаря автозаполнению LOCATION уменьшаются клики и «срыв» формы — это снижает отказ и повышает CR в чекауте.
  • Локализованные тексты доставки. Зная город, можно сразу показывать релевантные сроки/стоимость доставки и самовывозы — CTR по пунктам выдачи обычно растёт.
  • Ясные подсказки. Если подставили город автоматически, добавьте рядом «Это не ваш город?» → «Изменить» — повышает доверие и уменьшает раздражение.

Резюме

  • Мы подписались на событие OnSaleComponentOrderProperties и автозаполняем LOCATION в bitrix:sale.order.ajax, если пользователь сам его не трогал.
  • В базовой версии берём город из профиля либо по IP через внешний сервис; в улучшенной — используем встроенный GeoIP и корректный поиск локации по названию.
  • Следуем принципу «не вредить»: пользователь всегда может выбрать другой город.
Теги:  sale.order.ajax, автозаполнение города, определение местоположения, GeoIP, PHP, интернет-магазин, конверсия


Стоимость услуг по разработке и сопровождению сайтов на 1C-Битрикс

Перенос сайтов на «1С-Битрикс»

сайты на платформе «1С-Битрикс» — это удобство, надежность и высокая посещаемость

от 12 000 рублей
Перенос сайтов с любых CMS и статичных страниц на платформу «1С-Битрикс», с учетом дизайна, верстки и урл-адресов. С сохранением всей информации и структуры сайта.

* зависит от объема выполняемых работ.

Интернет-магазин на готовом решении

от 7 дней

от 40 000 рублей
запуск сайта в максимально короткие сроки

* указана минимальная стоимость. Стоимость выбранной лицензии «1С-Битрикс» оплачивается отдельно.

Участие в проекте

привлечение в проект на part-time основе

от 30 000 рублей / неделя

Возможно участие в проекте на ежедневной основе, как разработчика. Занятость - до 20 часов в неделю
Минимальный срок - одна неделя.

* сумма фиксированная