Список пользователей с городами в Bitrix
Введение
В этой статье разберём, как создать публичную страницу в 1С-Битрикс, которая выводит список активных пользователей с определением их города из заказов. Страница включает фильтр по названию города, постраничную навигацию и экспорт результатов в Excel.
Это типичная задача для административных отчётов: менеджеру нужно понимать,
из каких городов приходят клиенты, сколько их, и выгрузить список для маркетинговой
рассылки. Город берётся из свойства LOCATION последнего заказа пользователя —
то есть используется реальный адрес доставки, а не просто профиль.
Код размещается в публичной части сайта — например, в файле
/users.php в корне. Он работает как отдельная страница с подключением
заголовка и футера Битрикса.
Быстрый старт
| Компонент | Значение |
|---|---|
| Файл | /users.php в корне сайта |
| Модуль | sale — для работы с заказами и местоположениями |
| Классы | CSaleOrder, CSaleOrderPropsValue, CSaleLocation, UserTable |
| Фильтр | ?CITY=Москва — поиск по названию города |
| Пагинация | 50 записей на страницу, PageNavigation для фильтра, NAV_PARAMS для полного списка |
| Экспорт | ?EXPORT=Y — выгрузка в XLS без пагинации |
Постановка задачи
Необходимо вывести страницу со следующими возможностями:
- Показать всех активных пользователей с их именем, email, телефоном и городом.
- Город определять из свойства
LOCATIONпоследнего заказа пользователя. - Реализовать фильтрацию по названию города (частичное совпадение).
- Добавить постраничную навигацию (50 записей на страницу).
- Реализовать экспорт отфильтрованных или всех данных в Excel.
Сложность в том, что город не хранится напрямую в таблице пользователей — его нужно получать через заказы и местоположения, что требует нескольких запросов к БД. Код написан так, чтобы минимизировать количество запросов за счёт кеширования в памяти.
Структура страницы
Страница состоит из трёх логических блоков: PHP-логика (подготовка данных), HTML-шаблон (форма и таблица) и CSS-стили. Весь код находится в одном файле, что удобно для быстрого развёртывания.
1. Подключение заголовка и инициализация
В начале страницы подключаем пролог Битрикса, задаём заголовок и подключаем
модуль sale — он необходим для работы с заказами и местоположениями.
<?php
ob_start();
require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
$APPLICATION->SetTitle("Пользователи и города");
CModule::IncludeModule('sale');ob_start() включает буферизацию вывода — это нужно, чтобы иметь
возможность чистить буфер при экспорте в Excel (иначе заголовки HTML-страницы
попадут в файл).
2. Получение параметров фильтра и пагинации
Извлекаем GET-параметры: фильтр по городу и номер страницы для постраничной навигации.
$cityFilter = trim($_GET["CITY"]);
$page = isset($_GET['PAGEN_1']) ? intval($_GET['PAGEN_1']) : 1;
if ($page < 1) $page = 1;
$perPage = 50;
$_REQUEST['PAGEN_1'] = $page;$_REQUEST['PAGEN_1'] = $page — это важно для компонента постраничной
навигации, который использует эту переменную для определения текущей страницы.
Основная логика: получение данных
Дальше код разветвляется в зависимости от того, задан ли фильтр по городу.
Режим A: фильтр по городу задан
Если передан CITY, нужно найти всех пользователей, у которых хотя бы
в одном заказе город совпадает с запросом. Алгоритм:
- Перебор всех заказов — получаем ID и USER_ID каждого заказа.
- Чтение свойства LOCATION — для каждого заказа проверяем свойство
с кодом
LOCATIONи получаем ID местоположения. - Кеширование городов — используем массив
$locCityCache, чтобы не делать повторный запрос кCSaleLocation::GetByIDдля одного и того же ID. - Совпадение по названию — сравниваем название города с
$cityFilterчерезstripos()(регистронезависимое вхождение). - Сбор ID пользователей — сохраняем ID подходящих пользователей
в
$filterUserIds.
LOCATION или использовать агент для предрасчёта
города пользователя в отдельное поле.
После сбора ID загружаем данных пользователей порциями, с постраничной навигацией:
$userIds = array_values($filterUserIds);
$totalCount = count($userIds);
$offset = ($page - 1) * $perPage;
$pageIds = array_slice($userIds, $offset, $perPage);
$userDb = \Bitrix\Main\UserTable::getList(array(
"filter" => array("=ID" => $pageIds, "=ACTIVE" => "Y"),
"select" => array("ID", "EMAIL", "NAME", "LAST_NAME", "SECOND_NAME", "PERSONAL_PHONE"),
"order" => array("ID" => "ASC"),
));
Для пагинации в этом режиме используется Bitrix\Main\UI\PageNavigation —
современный класс, который принимает общее количество записей и самостоятельно
рассчитывает страницы.
Режим B: без фильтра — полный список
Если фильтр не задан, выгружаем всех активных пользователей с пагинацией через
старый добрый CUser::GetList:
$userDb = CUser::GetList(
array("ID" => "ASC"),
"",
array("ACTIVE" => "Y"),
array("NAV_PARAMS" => array("nPageSize" => $perPage, "iNumPage" => $page)),
array("ID", "EMAIL", "NAME", "LAST_NAME", "SECOND_NAME", "PERSONAL_PHONE")
);Подгрузка городов для пользователей
Когда фильтр не задан, города догружаются отдельно — одним запросом собираем
все заказы для найденных пользователей, для каждого заказа проверяем свойство
LOCATION и извлекаем название города. При этом берется первый
(последний по ID) заказ пользователя, в котором заполнен адрес:
$orderDb = CSaleOrder::GetList(
array("ID" => "DESC"),
array("USER_ID" => array_keys($users)),
false,
false,
array("ID", "USER_ID")
);
$userOrders = array();
while ($order = $orderDb->Fetch()) {
$userOrders[$order["USER_ID"]][] = $order["ID"];
}
foreach ($userOrders as $userId => $orderIds) {
foreach ($orderIds as $oid) {
$propsDb = CSaleOrderPropsValue::GetOrderProps($oid);
while ($prop = $propsDb->Fetch()) {
if ($prop["CODE"] == "LOCATION") {
$loc = CSaleLocation::GetByID(intval($prop["VALUE"]));
$users[$userId]["CITY"] = $loc["CITY_NAME"] ?: $prop["VALUE"];
break 2;
}
}
}
}
Обратите внимание: break 2 прерывает сразу два цикла — по заказам
пользователя и по свойствам заказа. Как только нашли город — остальные заказы
этого пользователя не проверяем.
Экспорт в Excel
При передаче параметра ?EXPORT=Y страница формирует XLS-файл —
по сути, HTML-таблицу с заголовками Content-Type: application/vnd.ms-excel.
Excel открывает такие файлы без ошибок.
Ключевые моменты:
- Очистка буфера:
while (ob_get_level()) ob_end_clean()удаляет весь накопленный HTML-вывод, чтобы в файл попали только данные таблицы. - Заголовки HTTP:
Content-Type: application/vnd.ms-excel; charset=utf-8иContent-Disposition: attachment; filename=users.xlsзаставляют браузер скачать файл. - Кодировка:
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">— Excel корректно отобразит кириллицу. - Полная выгрузка: При экспорте игнорируется пагинация — выгружаются все пользователи, подходящие под фильтр.
?CITY=...&EXPORT=Y)
выгружаются только пользователи из найденного города. Без фильтра — все активные
пользователи. После отправки файла вызывается die(), чтобы Битрикс
не подмешал в вывод футер или другую обёртку.
Шаблон: форма, таблица, пагинация
После PHP-логики идёт HTML-разметка со вставками PHP для вывода данных.
Форма фильтра
Простая форма с текстовым полем для названия города и кнопкой «Найти». Если фильтр активен, показывается ссылка «Сбросить» и ссылка на скачивание Excel.
Таблица пользователей
Выводится пять колонок: ID, Имя, Email, Телефон, Город. Пустые значения заменяются
на — (тире). Если пользователи не найдены — сообщение об этом.
Все значения экранируются через htmlspecialcharsbx() для защиты от XSS.
Постраничная навигация
Используется компонент bitrix:system.pagenavigation:
-
При фильтре — передаётся объект
$nav(PageNavigation). -
Без фильтра — передаётся результат
$userDb(CUser::GetList), который сам хранит информацию о пагинации.
<div class="pagination">
<? if (isset($nav)): ?>
<? $APPLICATION->IncludeComponent("bitrix:system.pagenavigation", ".default", array(
"NAV_OBJECT" => $nav,
"SHOW_ALWAYS" => "Y",
), false, array("HIDE_ICONS" => "Y")); ?>
<? elseif (isset($userDb)): ?>
<? $APPLICATION->IncludeComponent("bitrix:system.pagenavigation", ".default", array(
"NAV_RESULT" => $userDb,
"SHOW_ALWAYS" => "Y",
), false, array("HIDE_ICONS" => "Y")); ?>
<? endif; ?>
</div>Полный код для копирования
Готовый файл users.php, который можно разместить в корне сайта:
<?
ob_start();
require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
$APPLICATION->SetTitle("Пользователи и города");
CModule::IncludeModule('sale');
$cityFilter = trim($_GET["CITY"]);
$page = isset($_GET['PAGEN_1']) ? intval($_GET['PAGEN_1']) : 1;
if ($page < 1) $page = 1;
$perPage = 50;
$_REQUEST['PAGEN_1'] = $page;
$filterUserIds = array();
$filterCityNames = array();
if ($cityFilter) {
$locCityCache = array();
$orderDb = CSaleOrder::GetList(
array("ID" => "DESC"),
array(),
false,
false,
array("ID", "USER_ID")
);
while ($order = $orderDb->Fetch()) {
$propsDb = CSaleOrderPropsValue::GetOrderProps($order["ID"]);
while ($prop = $propsDb->Fetch()) {
if ($prop["CODE"] == "LOCATION") {
$lid = $prop["VALUE"];
if (!isset($locCityCache[$lid])) {
$loc = CSaleLocation::GetByID(intval($lid));
$locCityCache[$lid] = $loc ? $loc["CITY_NAME"] : "";
}
if ($locCityCache[$lid] !== "" && stripos($locCityCache[$lid], $cityFilter) !== false) {
$filterUserIds[$order["USER_ID"]] = $order["USER_ID"];
$filterCityNames[$order["USER_ID"]] = $locCityCache[$lid];
}
break;
}
}
}
$users = array();
if (!empty($filterUserIds)) {
$userIds = array_values($filterUserIds);
$totalCount = count($userIds);
$offset = ($page - 1) * $perPage;
$pageIds = array_slice($userIds, $offset, $perPage);
$userDb = \Bitrix\Main\UserTable::getList(array(
"filter" => array("=ID" => $pageIds, "=ACTIVE" => "Y"),
"select" => array("ID", "EMAIL", "NAME", "LAST_NAME", "SECOND_NAME", "PERSONAL_PHONE"),
"order" => array("ID" => "ASC"),
));
while ($user = $userDb->fetch()) {
$users[$user["ID"]] = array(
"ID" => $user["ID"],
"EMAIL" => $user["EMAIL"],
"NAME" => trim($user["NAME"]." ".$user["LAST_NAME"]." ".$user["SECOND_NAME"]),
"PHONE" => $user["PERSONAL_PHONE"],
"CITY" => isset($filterCityNames[$user["ID"]]) ? $filterCityNames[$user["ID"]] : ""
);
}
$nav = new \Bitrix\Main\UI\PageNavigation("PAGEN_1");
$nav->setPageSize($perPage)->setCurrentPage($page)->setRecordCount($totalCount);
}
} else {
$userDb = CUser::GetList(
array("ID" => "ASC"),
"",
array("ACTIVE" => "Y"),
array("NAV_PARAMS" => array("nPageSize" => $perPage, "iNumPage" => $page)),
array("ID", "EMAIL", "NAME", "LAST_NAME", "SECOND_NAME", "PERSONAL_PHONE")
);
$users = array();
while ($user = $userDb->Fetch()) {
$users[$user["ID"]] = array(
"ID" => $user["ID"],
"EMAIL" => $user["EMAIL"],
"NAME" => trim($user["NAME"]." ".$user["LAST_NAME"]." ".$user["SECOND_NAME"]),
"PHONE" => $user["PERSONAL_PHONE"],
"CITY" => ""
);
}
if (!empty($users)) {
$orderDb = CSaleOrder::GetList(
array("ID" => "DESC"),
array("USER_ID" => array_keys($users)),
false,
false,
array("ID", "USER_ID")
);
$userOrders = array();
while ($order = $orderDb->Fetch()) {
$userOrders[$order["USER_ID"]][] = $order["ID"];
}
foreach ($userOrders as $userId => $orderIds) {
foreach ($orderIds as $oid) {
$propsDb = CSaleOrderPropsValue::GetOrderProps($oid);
while ($prop = $propsDb->Fetch()) {
if ($prop["CODE"] == "LOCATION") {
$loc = CSaleLocation::GetByID(intval($prop["VALUE"]));
$users[$userId]["CITY"] = $loc["CITY_NAME"] ?: $prop["VALUE"];
break 2;
}
}
}
}
}
}
// Экспорт в Excel
if (isset($_GET["EXPORT"]) && $_GET["EXPORT"] == "Y") {
while (ob_get_level()) ob_end_clean();
header("Content-Type: application/vnd.ms-excel; charset=utf-8");
header("Content-Disposition: attachment; filename=users.xls");
echo '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>';
echo '<table><tr><th>ID</th><th>Имя</th><th>Email</th><th>Телефон</th><th>Город</th></tr>';
$exportUsers = array();
if ($cityFilter) {
if (!empty($filterUserIds)) {
$userIds = array_values($filterUserIds);
$userDb = \Bitrix\Main\UserTable::getList(array(
"filter" => array("=ID" => $userIds, "=ACTIVE" => "Y"),
"select" => array("ID", "EMAIL", "NAME", "LAST_NAME", "SECOND_NAME", "PERSONAL_PHONE"),
"order" => array("ID" => "ASC"),
));
while ($user = $userDb->fetch()) {
$exportUsers[$user["ID"]] = array(
"ID" => $user["ID"],
"EMAIL" => $user["EMAIL"],
"NAME" => trim($user["NAME"]." ".$user["LAST_NAME"]." ".$user["SECOND_NAME"]),
"PHONE" => $user["PERSONAL_PHONE"],
"CITY" => isset($filterCityNames[$user["ID"]]) ? $filterCityNames[$user["ID"]] : ""
);
}
}
} else {
$userDb = CUser::GetList(
array("ID" => "ASC"),
"",
array("ACTIVE" => "Y"),
false,
array("ID", "EMAIL", "NAME", "LAST_NAME", "SECOND_NAME", "PERSONAL_PHONE")
);
while ($user = $userDb->Fetch()) {
$exportUsers[$user["ID"]] = array(
"ID" => $user["ID"],
"EMAIL" => $user["EMAIL"],
"NAME" => trim($user["NAME"]." ".$user["LAST_NAME"]." ".$user["SECOND_NAME"]),
"PHONE" => $user["PERSONAL_PHONE"],
"CITY" => ""
);
}
if (!empty($exportUsers)) {
$orderDb = CSaleOrder::GetList(
array("ID" => "DESC"),
array("USER_ID" => array_keys($exportUsers)),
false,
false,
array("ID", "USER_ID")
);
$userOrders = array();
while ($order = $orderDb->Fetch()) {
$userOrders[$order["USER_ID"]][] = $order["ID"];
}
foreach ($userOrders as $userId => $orderIds) {
foreach ($orderIds as $oid) {
$propsDb = CSaleOrderPropsValue::GetOrderProps($oid);
while ($prop = $propsDb->Fetch()) {
if ($prop["CODE"] == "LOCATION") {
$loc = CSaleLocation::GetByID(intval($prop["VALUE"]));
$exportUsers[$userId]["CITY"] = $loc["CITY_NAME"] ?: $prop["VALUE"];
break 2;
}
}
}
}
}
}
foreach ($exportUsers as $u):
?><tr><td><?= $u["ID"] ?></td><td><?= htmlspecialcharsbx($u["NAME"]) ?></td><td><?= htmlspecialcharsbx($u["EMAIL"]) ?></td><td><?= htmlspecialcharsbx($u["PHONE"]) ?></td><td><?= htmlspecialcharsbx($u["CITY"]) ?></td></tr>
<? endforeach;
echo '</table></body></html>';
die();
}
?>
<style>
table.users-table { width: 100%; border-collapse: collapse; }
table.users-table th, table.users-table td { padding: 10px; text-align: left; border: 1px solid #ddd; }
table.users-table th { background: #f5f5f5; font-weight: bold; }
table.users-table tr:nth-child(even) { background: #fafafa; }
.filter-form { margin: 20px 0; padding: 15px; background: #f9f9f9; border: 1px solid #ddd; border-radius: 4px; }
.filter-form input[type="text"] { padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; width: 250px; }
.filter-form input[type="submit"] { padding: 8px 20px; background: #0073aa; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
.filter-form input[type="submit"]:hover { background: #005a87; }
</style>
<h1>Список пользователей с городами</h1>
<form class="filter-form" method="get">
<label>Фильтр по городу: </label>
<input type="text" name="CITY" value="<?= htmlspecialcharsbx($cityFilter) ?>" placeholder="Введите название города">
<input type="submit" value="Найти">
<? if ($cityFilter): ?>
<a href="?" style="margin-left: 10px;">Сбросить</a>
<? endif; ?>
<a href="<?= $cityFilter ? "?CITY=".urlencode($cityFilter)."&EXPORT=Y" : "?EXPORT=Y" ?>" style="margin-left: 10px;">Скачать Excel</a>
</form>
<table class="users-table">
<tr>
<th>ID</th>
<th>Имя</th>
<th>Email</th>
<th>Телефон</th>
<th>Город</th>
</tr>
<? foreach ($users as $user): ?>
<tr>
<td><?= $user["ID"] ?></td>
<td><?= htmlspecialcharsbx($user["NAME"]) ?></td>
<td><?= htmlspecialcharsbx($user["EMAIL"]) ?></td>
<td><?= htmlspecialcharsbx($user["PHONE"]) ?: "—" ?></td>
<td><?= htmlspecialcharsbx($user["CITY"]) ?: "—" ?></td>
</tr>
<? endforeach; ?>
<? if (empty($users)): ?>
<tr><td colspan="5" style="text-align:center; color:#999;">Пользователи не найдены</td></tr>
<? endif; ?>
</table>
<div class="pagination">
<? if (isset($nav)): ?>
<? $APPLICATION->IncludeComponent("bitrix:system.pagenavigation", ".default", array(
"NAV_OBJECT" => $nav,
"SHOW_ALWAYS" => "Y",
), false, array("HIDE_ICONS" => "Y")); ?>
<? elseif (isset($userDb)): ?>
<? $APPLICATION->IncludeComponent("bitrix:system.pagenavigation", ".default", array(
"NAV_RESULT" => $userDb,
"SHOW_ALWAYS" => "Y",
), false, array("HIDE_ICONS" => "Y")); ?>
<? endif; ?>
</div>
<?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");?>Файл готов к размещению. Откройте в браузере http://вашсайт/users.php — должна отобразиться таблица с пользователями.
Заключение
Мы создали полноценную публичную страницу на 1С-Битрикс, которая решает реальную бизнес-задачу: показывает менеджеру список пользователей с их городами из заказов, позволяет фильтровать по городу, листать страницы и скачивать результат в Excel.
Код использует как классы старого ядра (CSaleOrder, CUser),
так и современное D7 API (UserTable, PageNavigation) —
это наглядный пример того, как в Битриксе можно сочетать оба подхода в одном файле.
Часто задаваемые вопросы
Создайте файл в корне сайта, подключите заголовок Битрикса, получите список активных пользователей через CUser::GetList или UserTable, затем для каждого пользователя найдите его заказы и из свойства LOCATION извлеките город через CSaleLocation::GetByID.
Переберите все заказы, для каждого получите свойство LOCATION и сравните название города с фильтром через stripos(). Соберите ID подходящих пользователей в массив и загрузите их данные. Готовый код — в разделе «Полный код для копирования».
Установите заголовки Content-Type: application/vnd.ms-excel и Content-Disposition: attachment; filename=...xls, очистите буфер вывода и выведите HTML-таблицу. Excel корректно открывает такие файлы. Не забудьте указать charset=utf-8 в meta-теге.
CSaleLocation::GetByID($id) — получает массив с данными местоположения по его ID, включая CITY_NAME. ID местоположения хранится в свойстве заказа с кодом LOCATION.
Создайте объект new \Bitrix\Main\UI\PageNavigation("PAGEN_1"), передайте ему размер страницы, текущую страницу и общее количество записей через методы setPageSize(), setCurrentPage(), setRecordCount(). Затем передайте его в компонент bitrix:system.pagenavigation в параметре NAV_OBJECT.
Ключевые моменты
- Модуль Перед работой с заказами и местоположениями обязательно подключите модуль
saleчерезCModule::IncludeModule('sale') - Город
CSaleLocation::GetByID()принимает ID местоположения и возвращает массив с ключомCITY_NAME - Кеш Кешируйте результат
GetByIDв памяти — это ускоряет перебор тысяч заказов - Экспорт Перед выгрузкой Excel очищайте буфер (
ob_end_clean) и завершайте скрипт черезdie() - Пагинация Для
PageNavigationиспользуйтеNAV_OBJECT, дляCUser::GetList—NAV_RESULT
