Задача из 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 про такое экранирование всё равно не знает.
Грабли
Content-type: application/vnd.ms-excel и именем items_export.xls. Excel такой файл открывает, но сначала пугает: «формат и расширение файла не совпадают, файл может быть повреждён или небезопасен». Покупатель, увидевший это на сайте магазина, скорее закроет файл, чем нажмёт «Да». CSV такого предупреждения не вызывает. Если нужен настоящий .xlsx с форматированием — это библиотека PhpSpreadsheet, но для прайса из трёх колонок она избыточна.EF BB BF — и есть строчка fwrite( $out, "\xEF\xBB\xBF" ).;.А если всё-таки через 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.