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 напрямую и остаются на диске как обычные файлы - они не сломают сайт, но со временем занимают место, поэтому на проектах с частой заменой картинок эту папку стоит периодически чистить отдельно от удаления самих файлов.