Экспорт в CSV: карта колонок, конвертеры и потоковая выдача в браузер
Зачем карта полей
Учебный пример с жёстко прошитыми ключами массива быстро превращается в копипасту: при добавлении поля правят и SQL, и шапку, и тело строки. Удобнее держать карту колонок: из базы берём сырой набор полей, а заголовки CSV и преобразования значений описываем один раз в конфигурации — по аналогии с массивом exportedValues из классической заметки про выгрузку пользователей.
Поток и память
Открывайте fopen('php://output', 'w') и пишите строки по мере чтения батчей из БД (например, по 100–500 строк). Так вы не раздуваете память на десятки тысяч записей. Заголовки HTTP отправляйте до любого вывода тела; в конце die() или exit в обработчике допустимы, если дальше по цепочке не должно идти HTML от шаблона.
Заголовки и Excel
Тройное Content-Type из старых примеров браузеру не нужно: достаточно типа для скачивания. Для кириллицы в Microsoft Excel разумно отдать UTF‑8 с BOM в начале потока (\xEF\xBB\xBF) и явно указать text/csv; charset=UTF-8. Имя файла в Content-Disposition задавайте с расширением .csv, если внутри действительно CSV — переименование в .xls только путает пользователей и антивирусы.
Разделитель и кавычки
fputcsv экранирует поля по правилам RFC; третий аргумент — разделитель (в РФ часто ; для Excel при локали с запятой как десятичным разделителем). При необходимости задайте enclosure и escape согласно документации PHP.
Пример: карта + конвертеры + батчи
Ниже — самодостаточный скелет без привязки к конкретному фреймворку. Источник данных подмените на свой (в «1С‑Битрикс» для пользователей уместен ORM Bitrix\Main\UserTable с фильтром и постраничкой через параметры limit и offset у getList).
declare(strict_types=1);
final class CsvExporter
{
/** @var resource */
private $stream;
/** @var list<array{column:string,name:string,function?:string}> */
private array $mapping;
/**
* @param callable(int,int): iterable<array<string,mixed>>|false $loadPage
*/
public function __construct(
private string $downloadFileName,
array $mapping,
private $loadPage
) {
$this->mapping = $mapping;
}
public function send(): void
{
$this->sendHeaders($this->downloadFileName);
$this->stream = fopen('php://output', 'w');
if ($this->stream === false) {
return;
}
fwrite($this->stream, "\xEF\xBB\xBF");
$this->writeHead();
$offset = 0;
$limit = 200;
while (($rows = ($this->loadPage)($offset, $limit)) !== false) {
$batch = iterator_to_array($rows, false);
if ($batch === []) {
break;
}
foreach ($batch as $row) {
$this->writeRow($row);
}
$offset += $limit;
}
fclose($this->stream);
}
private function sendHeaders(string $fileName): void
{
header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="' . basename($fileName) . '"');
header('Cache-Control: no-store, private');
}
private function writeHead(): void
{
$head = [];
foreach ($this->mapping as $col) {
$head[] = $col['name'];
}
fputcsv($this->stream, $head, ';');
}
private function writeRow(array $row): void
{
$out = [];
foreach ($this->mapping as $col) {
$raw = $row[$col['column']] ?? null;
$fn = $col['function'] ?? null;
$out[] = ($fn !== null && is_callable([ValueConverter::class, $fn]))
? ValueConverter::{$fn}($raw)
: (string) ($raw ?? '');
}
fputcsv($this->stream, $out, ';');
}
}
final class ValueConverter
{
public static function idate(mixed $v): string
{
$t = (int) $v;
return $t <= 0 ? '—' : date('d.m.Y', $t);
}
public static function isBool(mixed $v): string
{
return ((bool) $v) ? 'да' : 'нет';
}
}Про пароли и персональные данные
Выгружать хеш пароля или чувствительные поля в файл — плохая идея даже для администратора: это расширяет поверхность утечки. Оставляйте в CSV только то, что реально нужно процессу; остальное вырезайте на уровне SQL или через карту колонок.
Кратко
- Карта колонок отвязывает запрос к данным от заголовков и форматирования.
- Батчи и поток в
php://outputсохраняют память на больших выборках. - Для Excel и UTF‑8 используйте BOM и корректный
Content-Type. - Не включайте в выгрузку секреты без крайней необходимости.
Не хотите копаться сами?
Починю за 1-3 дня. Без предоплаты — оплата по результату.
15+ лет опыта с 1С-Битрикс · Без предоплаты · 7 дней гарантии