1С Битрикс · 8 мин чтения

CFile в Битрикс: загрузка, ресайз и вывод картинок из кода

CFile в Битрикс - класс ядра, через который проходит любая работа с файлами: сохранить загруженную картинку, получить уменьшенную копию под нужный размер, вывести превью в кастомном компоненте. В админке за это отвечает визуальный редактор, но как только нужно загрузить файл из своего скрипта, обработать импорт из 1С или вывести картинку на странице, собранной руками, без CFile не обойтись. Я работаю с проектами на Битриксе, где каталоги на несколько тысяч товаров, и обычно первым делом лезу именно в этот класс, а не в готовые компоненты - так проще контролировать, что происходит с файлами на диске. В статье - рабочие примеры: как сохранить файл, сделать ресайз и вывести картинку без лишних плагинов.

Что делает CFile и где Битрикс хранит файлы

Каждый загруженный файл в Битриксе описывается двумя вещами: физическим файлом в папке /upload и записью в таблице b_file с его метаданными - именем, размером, MIME-типом, путём и служебным полем SUBDIR, по которому Битрикс раскладывает файлы по подпапкам с датой загрузки, чтобы в одной директории не скапливались тысячи файлов. Все ссылки на файлы в проекте, будь то PREVIEW_PICTURE и DETAIL_PICTURE у элемента инфоблока, PERSONAL_PHOTO у пользователя или кастомное свойство типа «Файл» в highload-блоке, хранят не путь, а числовой ID из этой таблицы.

CFile работает с двумя состояниями файла. До сохранения это массив с ключами name, type, tmp_name, size - точно такой же формат, в каком PHP отдаёт $_FILES, либо результат CFile::MakeFileArray для файла, который уже лежит на диске. После вызова CFile::SaveFile массив превращается в целое число - ID, который дальше передаётся куда угодно: в свойство инфоблока, в поле пользователя, в свою таблицу с произвольной структурой.

Это разделение состояний и есть источник половины ошибок у новичков: пытаются вывести картинку по массиву, который ещё не сохранён, или наоборот передают в SaveFile уже готовый числовой ID вместо массива.

Загрузка файла через CFile::SaveFile

Самый частый случай - обработка формы с полем file. Проверяю, что файл реально пришёл, и только тогда сохраняю. Вот как это выглядит в обработчике формы:

if ($_FILES["PREVIEW_IMAGE"]["name"] != "") {
    $fileId = CFile::SaveFile($_FILES["PREVIEW_IMAGE"], "iblock");
    if ($fileId) {
        CIBlockElement::SetPropertyValueCode($elementId, "PHOTO", $fileId);
    }
}

Второй параметр SaveFile - save_path, в документации это «Путь к папке в которой хранятся файлы (относительно папки /upload)». Он и определяет подпапку внутри /upload, куда физически ляжет файл: iblock, catalog, main и так далее. Название модуля вторым аргументом не передаётся: MODULE_ID это отдельный ключ внутри массива с данными файла, рядом с name, size, tmp_name и type. Если под отдельный функционал задать свой save_path, его файлы не будут перемешиваться с остальными - на проекте с кастомным модулем учёта документов я обычно завожу отдельную папку вроде «docs.storage», чтобы потом было легко почистить именно её при необходимости.

Второй сценарий - когда файл уже лежит на сервере: пришёл через импорт из 1С, скачан по расписанию из внешнего источника или сгенерирован скриптом. Тут нужен CFile::MakeFileArray, который по пути на диске собирает массив в формате, понятном SaveFile. Выглядит это так:

$filePath = $_SERVER["DOCUMENT_ROOT"]."/upload/tmp/import/photo.jpg";
$arFile = CFile::MakeFileArray($filePath);
if (is_array($arFile)) {
    $fileId = CFile::SaveFile($arFile, "catalog");
}

CFile::SaveFile возвращает false, если файл не передан или произошла ошибка записи - эту проверку я никогда не пропускаю, иначе в свойство инфоблока улетит false вместо ID и вывод картинки потом молча сломается на всех страницах, где используется это свойство.

Проверка файла перед сохранением

Для картинок в Битриксе есть отдельный метод CFile::CheckImageFile: «Метод проверяет что файл является картинкой и проверяет ее параметры. В случае ошибки метод вернет строку с текстом ошибки». Сигнатура такая: CFile::CheckImageFile(array file, int max_size = 0, int max_width = 0, int max_height = 0, array access_typies = array(), bool ForceMD5=false, bool SkipExt=false), то есть ограничить можно размер в байтах, ширину и высоту в пикселях, а параметра со списком расширений у метода нет. Вызываю его перед SaveFile и, если метод вернул текст ошибки, до сохранения дело не доходит. Проверка в коде выглядит так:

$maxSize = 5 * 1024 * 1024;
$error = CFile::CheckImageFile($_FILES["PREVIEW_IMAGE"], $maxSize, 4000, 4000);
if ($error == "") {
    $fileId = CFile::SaveFile($_FILES["PREVIEW_IMAGE"], "iblock");
} else {
    echo $error;
}

На форме с загрузкой обложки товара такая проверка отсекает случаи, когда менеджер по ошибке прикрепляет PDF с расширением jpg или картинку на несколько десятков мегабайт с телефона - без неё файл всё равно сохранится, а проблема всплывёт только на этапе ресайза или вывода.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Ресайз изображений: CFile::ResizeImageGet

Ресайз в Битриксе не создаёт новый файл в b_file - CFile::ResizeImageGet возвращает путь к копии в папке кэша /upload/resize_cache и держит эту копию столько, сколько существует кэш. При повторном вызове с теми же параметрами Битрикс просто отдаёт путь к уже готовому файлу, без повторной генерации, поэтому по скорости повторные обращения ничем не отличаются от обычной статики.

Третий параметр отвечает за то, как картинка впишется в заданные размеры:

Константа Как ведёт себя ресайз
BX_RESIZE_IMAGE_PROPORTIONAL Вписывает картинку в заданные размеры с сохранением пропорций, итоговая ширина или высота может оказаться меньше запрошенной
BX_RESIZE_IMAGE_EXACT Масштабирует в прямоугольник из width и height с сохранением пропорций, обрезая лишнее
BX_RESIZE_IMAGE_PROPORTIONAL_ALT Масштабирует с сохранением пропорций, за ширину принимается максимальное значение из высоты и ширины, размер ограничивается заданным; улучшенная обработка вертикальных картинок

На карточке товара мне обычно нужно несколько разных размеров одной картинки - миниатюра в списке, превью в галерее, крупная версия для лайтбокса. Вызываю ResizeImageGet три раза с разными width и height, а за производительность не переживаю - диск сам решает вопрос кэширования, и после первого посетителя все следующие получают уже готовые файлы. Базовый вызов выглядит так:

$arFileTmp = CFile::ResizeImageGet(
    $fileId,
    array("width" => 400, "height" => 300),
    BX_RESIZE_IMAGE_PROPORTIONAL,
    true
);
if ($arFileTmp) {
    echo '<img src="'.$arFileTmp["src"].'" width="'.$arFileTmp["width"].'" height="'.$arFileTmp["height"].'" alt="">';
}

Четвёртый параметр это bInitSizes, флаг возврата размеров уменьшенной копии: «True - возвращает, false - нет». Для тега img он ключевой, потому что в возвращаемом массиве «‘width’, // Если bInitSizes = true ширина уменьшенной картинки, иначе 0», то есть с false в атрибуты width и height уйдут нули. Поэтому когда из результата собирается img с размерами, четвёртым аргументом передаю true. Сам метод «В случае ошибки возвращает false», например когда оригинала уже нет на диске, и эту проверку я оставляю всегда.

Ресайзить на лету или заранее

Для страниц с большим трафиком, где важна каждая секунда до первой отрисовки, вызываю ResizeImageGet сразу после SaveFile - тогда нужные размеры попадают в кэш ещё на этапе сохранения товара в админке, а не при первом заходе живого посетителя на карточку. Для внутренних разделов с редким трафиком так не заморачиваюсь - разница на один лишний запрос незаметна.

Вывод картинок из кода: GetPath и GetFileArray

Когда нужен только путь к файлу без остальных данных, хватает CFile::GetPath - на входе ID, на выходе строка с адресом файла на сервере. Одной строкой:

$path = CFile::GetPath($fileId);
echo $path;

Если в шаблоне нужно собрать полноценный тег img с шириной и высотой, чтобы браузер не дёргал вёрстку при загрузке картинки, беру CFile::GetFileArray - он возвращает массив с ключами SRC, WIDTH, HEIGHT, FILE_SIZE, DESCRIPTION и другими метаданными файла. В шаблоне это выглядит так:

$arFile = CFile::GetFileArray($fileId);
if ($arFile) {
    echo '<img src="'.$arFile["SRC"].'" width="'.$arFile["WIDTH"].'" height="'.$arFile["HEIGHT"].'" alt="'.htmlspecialcharsbx($arFile["DESCRIPTION"]).'">';
}

DESCRIPTION в этом массиве - то самое поле описания файла, которое можно заполнить при загрузке через MakeFileArray или прямо в форме админки. Я обычно кладу туда alt-текст картинки, чтобы не заводить под это отдельное поле в инфоблоке и не дублировать данные.

Удаление файлов и очистка кэша ресайза

CFile::Delete удаляет и физический файл, и запись в b_file:

CFile::Delete($fileId);

Но важный момент: копии в /upload/resize_cache, которые нагенерировал ResizeImageGet, при этом не трогаются - Битрикс не ведёт им учёт в базе, это просто файлы на диске, которые пересоздаются заново при следующем запросе нужного размера.

На практике это значит, что после массовой замены картинок в каталоге или после редизайна карточки товара с новыми размерами превью папка resize_cache копится мусором - старые ресайзы уже никому не нужны, но сами по себе не удаляются. Я периодически чищу эту папку на проектах с большими каталогами, обычно вручную через FTP или скриптом по расписанию раз в месяц. Если нужно навести порядок в файлах и настройках на уже работающем проекте на Битриксе, я беру такие задачи в рамках технической поддержки сайта.

Типичные ошибки при работе с CFile

За несколько лет работы с Битриксом чаще всего вижу одни и те же грабли.

  • Ресайз на каждый рендер страницы без переменной для результата - ResizeImageGet вызывается по несколько раз за один HTTP-запрос с одинаковыми параметрами, хотя результат можно один раз положить в переменную компонента и переиспользовать в шаблоне.
  • Забытая проверка $_FILES на ошибку загрузки - SaveFile получает массив с error, отличным от нуля, из-за оборванной загрузки на медленном канале, и молча создаёт битую запись в базе.
  • Один и тот же BX_RESIZE_IMAGE_EXACT для логотипов и товарных фото с разными пропорциями - лишние края обрезаются, и у логотипа отъедает часть знака, хотя для него подходит BX_RESIZE_IMAGE_PROPORTIONAL, который вписывает картинку в заданные размеры целиком.
  • Один save_path на все типы файлов в проекте - через год в /upload/iblock лежат вперемешку картинки товаров, аватары и вложения из тикетов поддержки, и найти нужное превращается в отдельную задачу при переносе на новый сервер.

1С-Битрикс регулярно обновляется, поэтому перед внедрением я сверяюсь с актуальной документацией.

Частые вопросы

Как получить ID файла, если он уже привязан к элементу инфоблока?

ID лежит прямо в результате выборки элемента: для главной картинки это поля PREVIEW_PICTURE и DETAIL_PICTURE, для кастомного свойства типа «Файл» - значение соответствующего PROPERTY_ЧТО_ТО_VALUE. Это обычное целое число, его можно сразу передавать в CFile::GetFileArray или CFile::ResizeImageGet без дополнительных преобразований и без похода в базу за путём вручную.

Почему после ResizeImageGet картинка не появляется на странице?

Чаще всего дело в правах на папку /upload - после переноса проекта на новый сервер веб-сервер иногда не может писать в resize_cache, и функция возвращает false вместо массива с путём. Второй частый вариант - оригинальный файл удалили из b_file через CFile::Delete, а ссылка на его старый ID осталась в свойстве инфоблока или в закэшированном компоненте.

Можно ли сохранить файл, если он не загружается через форму, а уже лежит на сервере?

Да, для этого нужен CFile::MakeFileArray с путём к файлу на диске - он соберёт массив в формате, который понимает SaveFile. Если файл нужно сначала скачать по внешней ссылке, я обычно сохраняю его во временную папку скриптом, а затем прогоняю через MakeFileArray и SaveFile теми же двумя вызовами, что и для формы.

Нужно ли удалять ресайз-копии вручную после CFile::Delete?

CFile::Delete убирает только оригинал и запись в базе. Копии в /upload/resize_cache не связаны с этим ID напрямую и остаются на диске как обычные файлы - они не сломают сайт, но со временем занимают место, поэтому на проектах с частой заменой картинок эту папку стоит периодически чистить отдельно от удаления самих файлов.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно