Перейти к содержанию

Прайс-лист выбранных товаров в Excel: форма без JavaScript и CSV на PHP

Задача из 2023 года: на сайте список товаров, покупатель отмечает галочками нужные и по кнопке скачивает прайс-лист в Excel только с ними. Тогда я решил её костылём: скрытая форма, скрытое поле, JavaScript, который складывает туда номера товаров через запятую, и PHP-скрипт, который притворяется файлом .xls.

Работало. Но сейчас я бы сделал всё то же без единой строчки JavaScript, а файл отдавал бы в формате, на который Excel не ругается. Ниже — как, с кодом, который я проверил на PHP 8.5 и в Chrome.

Коротко: галочки и кнопка привязываются к форме атрибутом form="id-формы", даже если стоят вне её; форма с target="_blank" отправляет номера товаров PHP-скрипту; скрипт отдаёт CSV с BOM и точкой с запятой — Excel открывает его без вопросов.

Страница: галочки и кнопка без JavaScript

В 2023 году я прятал форму, потому что галочки стояли в таблице товаров, а не внутри формы. Оказалось, прятать ничего не нужно: у полей и кнопок есть атрибут form, который привязывает их к форме по её id, где бы они ни стояли на странице.

<table>
	<tr>
		<td><input type="checkbox" name="ids[]" value="7" form="price-form"></td>
		<td>Слон африканский</td>
	</tr>
	<tr>
		<td><input type="checkbox" name="ids[]" value="12" form="price-form"></td>
		<td>Слон индийский</td>
	</tr>
</table>

<form id="price-form" method="post" action="/price.php" target="_blank"></form>
<button form="price-form">Получить прайс</button>

Форма пустая, но отмеченные галочки уходят вместе с ней. Квадратные скобки в ids[] говорят PHP собрать все значения в массив. Проверил в Chrome: из трёх галочек, где отмечены первая и третья, форма отправляет 7,15 — ровно отмеченные. Раз ответ — файл с заголовком Content-Disposition: attachment, браузер его скачает и со страницы не уйдёт. target="_blank" нужен на случай ошибки: текст «Не выбрано ни одного товара» откроется в новой вкладке, а не заменит список товаров.

Скрипт: CSV, который Excel понимает

<?php
// Прайс-лист выбранных товаров в CSV для Excel.
$ids = array_filter( array_map( 'intval', (array) ( $_POST['ids'] ?? [] ) ) );
if ( ! $ids ) {
	http_response_code( 400 );
	exit( 'Не выбрано ни одного товара' );
}

// Здесь — выборка из своей базы по $ids. Для примера — массив.
$items = [
	[ 'id' => 7, 'name' => 'Слон африканский', 'price' => 1500000 ],
	[ 'id' => 12, 'name' => 'Слон индийский; малый', 'price' => 990000 ],
];
$items = array_filter( $items, fn( $i ) => in_array( $i['id'], $ids, true ) );

header( 'Content-Type: text/csv; charset=UTF-8' );
header( 'Content-Disposition: attachment; filename="price-' . date( 'Y-m-d' ) . '.csv"' );

$out = fopen( 'php://output', 'w' );
fwrite( $out, "\xEF\xBB\xBF" ); // BOM: без него Excel покажет кракозябры вместо русских букв
fputcsv( $out, [ 'Код', 'Товар', 'Цена, ₽' ], ';', '"', '' );
foreach ( $items as $i ) {
	fputcsv( $out, [ $i['id'], $i['name'], $i['price'] ], ';', '"', '' );
}

Что получается в файле:

Код;Товар;"Цена, ₽"
7;"Слон африканский";1500000
12;"Слон индийский; малый";990000

Пара мест, которые стоит понимать:

  • array_map( 'intval', … ) превращает всё пришедшее в числа, а array_filter выбрасывает нули. Мусор вроде ids[]=abc отсеивается, и в запрос к базе попадают только числа. Пустой выбор — ответ 400 с понятным текстом.
  • Точка с запятой как разделитель — потому что русский Excel ждёт именно её: в русской раскладке запятая занята под дробную часть числа. Если в названии товара встретится ;, fputcsv сам возьмёт поле в кавычки.
  • Последний параметр '' у fputcsv отключает экранирование обратной косой черты. Без него новые версии PHP предупреждают: Deprecated: fputcsv(): the $escape parameter must be provided as its default value will change (проверил на PHP 8.5). А Excel про такое экранирование всё равно не знает.

Грабли

Грабли №1: HTML с расширением .xls. В 2023 году скрипт отдавал HTML-таблицу с заголовком Content-type: application/vnd.ms-excel и именем items_export.xls. Excel такой файл открывает, но сначала пугает: «формат и расширение файла не совпадают, файл может быть повреждён или небезопасен». Покупатель, увидевший это на сайте магазина, скорее закроет файл, чем нажмёт «Да». CSV такого предупреждения не вызывает. Если нужен настоящий .xlsx с форматированием — это библиотека PhpSpreadsheet, но для прайса из трёх колонок она избыточна.
Грабли №2: кракозябры вместо русских букв. Без трёх байтов BOM в начале файла Excel открывает UTF-8 как текст в кодировке Windows-1251, и «Слон» превращается ⠫Слон». Эти три байта — EF BB BF — и есть строчка fwrite( $out, "\xEF\xBB\xBF" ).
Грабли №3: всё в одной колонке. Файл с запятыми вместо точек с запятой русский Excel покажет одной колонкой: «7,Слон африканский,1500000». Поэтому разделитель ;.

А если всё-таки через JavaScript

В первой версии поста я писал, что через ajax файл получить нельзя. Можно: fetch получает ответ как файл, а скачать его помогает временная ссылка. Это нужно, если перед скачиванием хочется показать ошибку на той же странице, а не в новой вкладке:

const body = new FormData(document.getElementById('price-form'));
const res = await fetch('/price.php', { method: 'POST', body });
if (!res.ok) {
	alert(await res.text()); // «Не выбрано ни одного товара»
} else {
	const name = /filename="([^"]+)"/.exec(res.headers.get('Content-Disposition'))?.[1] ?? 'price.csv';
	const link = document.createElement('a');
	link.href = URL.createObjectURL(await res.blob());
	link.download = name;
	link.click();
	URL.revokeObjectURL(link.href);
}

Запрос и разбор ответа я проверил с тем же PHP-скриптом: файл приходит с правильным именем, а пустой выбор возвращает ошибку 400 с текстом. Про атрибут download и заголовок Content-Disposition подробнее — в посте о том, как заставить браузер скачивать файл. А если прайс нужно, наоборот, получить из Excel в код, — пригодится разбор таблицы из Excel в массив PHP.

Написать комментарий

Адрес почты не публикуется. Обязательные поля отмечены звёздочкой.

Возможно, вы не читали

Как поставить Div поверх flash

1 февраля 2011
При верстке иногда хочется вставить flash в background, т.е. фоном, а поверх flash вставить что-нибудь другое. Ну хотя-бы как на этом сайте…

Блог или сайт? Что выбрать?

12 октября 2011
Формально блог - это тоже сайт. Да что там блог! Даже страничка слепленная в Ворде, сохраненная в формате html и присобаченная на…