Yandex Metrika
sanches.free

Импорт прайс-листа из Excel: PHPExcel, проверка шапки и обновление цен

Задача

Старая заметка с pushorigin.ru показывала разбор XLS: читать лист, сверить «шапку», пройти строки с номенклатурой и обновить цены и остатки в таблице shop_items. Ниже — тот же каркас, переписанный для реальной эксплуатации рядом с «1С‑Битрикс»: аккуратная работа с формулами в ячейках, валидация строки и отказ от небезопасной сборки SQL через addslashes.

PHPExcel и преемник

В примерах эпохи PHP 5 часто стоит PHPExcel. В новых проектах разумнее перейти на PhpSpreadsheet (наследник), оставив ту же идею: IOFactory::load, активный лист, чтение значений. Для совместимости с «тяжёлыми» книгами имеет смысл setReadDataOnly(true), если не нужны стили.

Проверка формата

Импорт должен обрываться на «чужом» файле. В оригинале контрольная ячейка — A4 с текстом вроде Номенклатура, Ед. изм.. Такой маркер дешевле, чем разбирать все варианты заголовков поставщика, но его стоит вынести в константу или конфиг, а не дублировать по коду.

Парсинг строк

Цикл начинается с фиксированной строки (в источнике — с 8-й): имя в колонке A, артикул в E, опция в F, цена в G, штрихкод в H, остаток в I. Строка считается годной, если заполнены имя, SKU и цена — иначе её лучше пропустить и залогировать, чем портить данные.

Если в ячейке лежит формула, текст из getValue() начинается с =. Для выгрузки поставщика обычно нужно то, что Excel уже пересчитал: в PHPExcel это getOldCalculatedValue(), в PhpSpreadsheet — актуальный пересчёт через getCalculatedValue() (или сохранённое значение, если пересчёт недоступен).

Класс загрузчика (скелет)

Ниже — структура без привязки к автозагрузчику; пути к библиотеке подключите сами. Исправлена синтаксическая ошибка из исходника: между элементами массива обязательна запятая после price.

final class PriceImporter
{
    private $sheet;

    public function import(string $file): int|false
    {
        $this->loadXls($file);
        if (!$this->isValidFormat()) {
            return false;
        }
        return $this->processWorkbook();
    }

    private function loadXls(string $fileName): void
    {
        $type = \PHPExcel_IOFactory::identify($fileName);
        $reader = \PHPExcel_IOFactory::createReader($type);
        $reader->setReadDataOnly(true);
        $book = $reader->load($fileName);
        $book->setActiveSheetIndex(0);
        $this->sheet = $book->getActiveSheet();
    }

    private function isValidFormat(): bool
    {
        return trim((string) $this->cell('A4')) === 'Номенклатура, Ед. изм.';
    }

    private function processWorkbook(): int
    {
        $line = 8;
        $done = 0;
        while (true) {
            $row = $this->readRow($line);
            if ($row === false) {
                break;
            }
            if ($row['ok']) {
                $item = new ShopItem($row, /* соединение с БД */);
                $done += $item->store();
            }
            $line++;
        }
        return $done;
    }

    /** @return array{ok:bool,...}|false */
    private function readRow(int $n)
    {
        if ($n > (int) $this->sheet->getHighestRow()) {
            return false;
        }
        $name = $this->cell('A' . $n);
        $sku = $this->cell('E' . $n);
        $price = $this->cell('G' . $n);
        return [
            'ok' => ($name !== '' && $sku !== '' && $price !== ''),
            'name' => $name,
            'sku' => $sku,
            'option' => $this->cell('F' . $n),
            'price' => $price,
            'bar' => $this->cell('H' . $n),
            'balance' => $this->cell('I' . $n),
        ];
    }

    private function cell(string $coord): string
    {
        $c = $this->sheet->getCell($coord);
        $v = $c->getValue();
        if (is_string($v) && $v !== '' && $v[0] === '=') {
            $v = $c->getOldCalculatedValue();
        }
        return trim((string) $v);
    }
}

Слой ShopItem: только обновление существующих

Исходная логика не создавала новые позиции — только находила id по SKU и обновляла прайс и остаток. Такой режим типичен, когда справочник номенклатуры заводится вручную, а Excel прилетает из учётной системы. Если нужно вставлять отсутствующие SKU, расширяют ветку «не найден» отдельным сценарием с валидацией и логированием.

Для «1С‑Битрикс» удобнее выполнять параметризованные запросы через \Bitrix\Main\Application::getConnection() и плейсхолдеры (или хотя бы SqlHelper::forSql), чем конкатенировать addslashes. Числовой id после выборки приводите к (int).

Где держать файл

В продакшене загрузку связывают с /upload/, проверкой расширения и размера, а запуск по cron — с тем же скриптом через пролог ядра (prolog_before.php), чтобы использовать те же константы подключения к БД, что и сайт.

Кратко

  • Проверяйте «свой» Excel маркером в шапке перед массовым UPDATE.
  • Для формул в ячейках берите вычисленное значение, а не строку с =.
  • Исправляйте синтаксис массивов и разделяйте загрузку, валидацию и запись по классам.
  • В Bitrix используйте слой БД ядра и безопасное экранирование вместо устаревшего слепого addslashes в SQL.

Не хотите копаться сами?

Починю за 1-3 дня. Без предоплаты — оплата по результату.

15+ лет опыта с 1С-Битрикс · Без предоплаты · 7 дней гарантии