Задача: форма с полем textarea, куда пользователь вставляет ячейки из Excel (Ctrl+C, Ctrl+V). Нужно превратить их в двумерный массив $arr[номер_строки][номер_ячейки].
В 2021 году я решил это делением по переводам строк и табуляциям, потом подпёр костылём, когда пользователи начали ставить переносы прямо в ячейках. Правильное решение оказалось короче обоих: в PHP уже есть функция, которая понимает этот формат.
Правильный способ: fgetcsv
Excel кладёт ячейки в буфер как TSV: табуляция между ячейками, перевод строки между строками. Ячейку, внутри которой есть перевод строки, табуляция или кавычка, он берёт в двойные кавычки, а кавычки внутри удваивает. Ровно по этим правилам работает CSV, только разделитель другой. Поэтому разбирать вставку надо CSV-парсером с разделителем "\t":
function tableToArray(string $text): array {
$f = fopen('php://memory', 'r+');
fwrite($f, $text);
rewind($f);
$rows = [];
while (($row = fgetcsv($f, 0, "\t", '"', '')) !== false) {
if ($row !== [null]) { // пустая строка в конце вставки
$rows[] = $row;
}
}
fclose($f);
return $rows;
}
Почему не str_getcsv: она разбирает одну строку, а текст сначала пришлось бы разделить по переводам строк — и ячейка с переносом снова развалится. fgetcsv читает поток и сама склеивает такие ячейки. Поток в памяти (php://memory) избавляет от временного файла.
Последний параметр '' отключает символ экранирования. По умолчанию это \, и обратный слэш в конце ячейки ломал бы разбор. С PHP 8.4 параметр стоит указывать явно, иначе будет предупреждение об устаревании.
Проверено на PHP 8.5: ячейка "Чёрный↵пакетики, ""25 шт""" приходит одной ячейкой Чёрный↵пакетики, "25 шт".
Как я делал в 2021-м
Первая версия — делим по переводам строк, потом по табуляциям:
function tableToArray($textarea) {
$arrtr = explode("\n", $textarea);
foreach ($arrtr as $k => $v) {
$arr[$k] = explode("\t", $v);
}
return $arr;
}
Ничто не предвещало беды, но вмешался человеческий фактор. Кое-кто повадился ставить переводы строк прямо в ячейках, и всё пошло по… лесу: одна ячейка превращалась в две строки массива.
Тогда я усложнил алгоритм. Считал число колонок по первой строке (шапку никто не правит, переносов в ней нет), делил весь текст по табуляциям, а последнюю ячейку каждой строки, слипшуюся с первой ячейкой следующей, разрезал по переводу строки. Заодно вычищал из ячеек переносы и спецсимволы:
function tableToArray($textarea) {
$prearrtr = explode("\n", $textarea);
$tdcount = count(explode("\t", $prearrtr[0])) - 1;
$tabattr = explode("\t", $textarea);
$forbiddenCharacters = "\/*?<>|+%!\n\r";
$arr = [];
$trnum = 0;
$tdnum = 0;
foreach ($tabattr as $v) {
if ($tdnum < $tdcount) {
$arr[$trnum][$tdnum] = preg_replace("/[${forbiddenCharacters}]/", '', $v);
$tdnum++;
} else {
$finalv = array_values(array_diff(explode("\n", $v), array('')));
$arr[$trnum][$tdnum] = preg_replace("/[${forbiddenCharacters}]/", '', $finalv[0]);
$trnum++;
$tdnum = 0;
$arr[$trnum][$tdnum] = preg_replace("/[${forbiddenCharacters}]/", '', $finalv[1]);
$tdnum++;
}
}
return $arr;
}
Работало, но на честном слове. Перенос в последней ячейке строки или в шапке, кавычки Excel в данных, строка с другим числом ячеек — и массив съезжал. А синтаксис ${…} внутри строки с PHP 8.2 считается устаревшим.
Урок: прежде чем писать свой разбор формата, стоит проверить, нет ли уже готового парсера. Почти всегда есть, и он уже прошёл через все грабли, на которые ты только собираешься наступить.