Каждый второй проект на Yii2 рано или поздно сталкивается с загрузкой файлов. Аватары, документы, изображения в галерею — без этого никуда. Но многие разработчики, особенно начинающие, делают это через раз, с копипастой из документации и велосипедами. Сегодня разберём, как правильно организовать файловое хранилище и загрузки в Yii2, чтобы не стрелять себе в ногу и не переписывать код через месяц.

Digital-студия WNDER

Почему не стоит хранить файлы в базе данных

Сразу к делу: забудьте про BLOB-поля в БД для файлов. Это моветон и боль. База данных раздувается, бэкапы становятся тяжёлыми, а производительность падает. Файлы должны лежать в файловой системе, а в БД — только путь к ним.

Представьте, что ваш сайт — это склад. База данных — это учётная книга, а файлы — это коробки с товаром. Если вы кладёте коробки прямо в книгу, она развалится. Так и здесь: храните в БД только ссылку на коробку, а саму коробку — на полке.

В Yii2 для этого есть удобный компонент — FileStorage, который входит в состав расширения yii2-imagine или можно написать свой. Но чаще всего используют просто атрибут модели с типом file и методы saveAs(). Однако для серьёзных проектов лучше сразу настроить нормальное хранилище.

Настройка файлового хранилища в Yii2

Yii2 не имеет встроенного файлового хранилища, но есть отличное расширение — creocoder/yii2-flysystem, которое позволяет работать с локальной файловой системой, S3, FTP и другими через единый интерфейс. Установка простая:

composer require creocoder/yii2-flysystem

Затем настраиваем компонент в конфигурации приложения (обычно это файл config/main.php):

'components' => [
    'fs' => [
        'class' => 'creocoder\flysystem\LocalFilesystem',
        'path' => '@webroot/uploads',
    ],
],

Теперь мы можем использовать компонент fs для работы с файлами. Например, чтобы сохранить файл:

$file = \Yii::$app->fs;
$file->write('path/to/file.txt', 'Содержимое файла');

А чтобы прочитать:

$content = $file->read('path/to/file.txt');

Всё просто и удобно. Но не забывайте, что для веб-доступа к файлам нужно настроить веб-сервер или использовать алиас @web/uploads.

Загрузка файлов через модели и формы

Теперь перейдём к самому интересному — загрузке файлов с помощью моделей и форм. В Yii2 это делается через валидатор file и метод upload().

Создадим модель загрузки файла:

namespace app\models;

use yii\base\Model;
use yii\web\UploadedFile;

class UploadForm extends Model
{
    public $file;

    public function rules()
    {
        return [
            [['file'], 'file', 'skipOnEmpty' => false, 'extensions' => 'png, jpg, pdf', 'maxSize' => 1024 * 1024 * 2],
        ];
    }

    public function upload()
    {
        if ($this->validate()) {
            $this->file->saveAs('uploads/' . $this->file->baseName . '.' . $this->file->extension);
            return true;
        } else {
            return false;
        }
    }
}

В контроллере обрабатываем загрузку:

public function actionUpload()
{
    $model = new UploadForm();

    if (\Yii::$app->request->isPost) {
        $model->file = UploadedFile::getInstance($model, 'file');
        if ($model->upload()) {
            // файл успешно загружен
            \Yii::$app->session->setFlash('success', 'Файл загружен!');
            return $this->redirect(['view']);
        }
    }

    return $this->render('upload', ['model' => $model]);
}

А во вьюхе делаем форму:

<?php
use yii\widgets\ActiveForm;
?>

<?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data']]) ?>

    <?= $form->field($model, 'file')->fileInput() ?>

    <button>Загрузить</button>

<?php ActiveForm::end() ?>

Обратите внимание на enctype — без него форма не отправит файл. Это типичная ошибка новичков.

Правильная организация хранилища и безопасность

Хранить все файлы в одной папке — плохая идея. Рано или поздно файлов станет много, и папка превратится в свалку. Лучше разбивать по подпапкам, например, по дате или по типу.

Также важно думать о безопасности. Никогда не доверяйте имени файла, которое отправляет пользователь. Злоумышленник может отправить имя с вредоносным расширением или с путями, типа ../../../etc/passwd. Поэтому всегда генерируйте новое имя файла сами, например, с помощью функции uniqid() или Yii::$app->security->generateRandomString().

Вот пример безопасной загрузки:

public function upload()
{
    if ($this->validate()) {
        $basePath = \Yii::getAlias('@webroot/uploads/');
        $fileName = \Yii::$app->security->generateRandomString(10) . '.' . $this->file->extension;
        $this->file->saveAs($basePath . $fileName);
        $this->filePath = 'uploads/' . $fileName;
        return true;
    }
    return false;
}

Кстати, проверяйте MIME-тип файла, а не только расширение. Расширение легко подделать. В Yii2 валидатор file имеет параметр mimeType — используйте его.

Правило: Валидируйте не только расширение, но и MIME-тип. И всегда переименовывайте файлы при сохранении. Это убережёт вас от многих проблем.

Скачивание файлов с контролем доступа

Иногда нужно отдавать файлы не напрямую, а через контроллер, чтобы проверить права пользователя. Например, если файл доступен только администраторам. В Yii2 это делается так:

public function actionDownload($id)
{
    $model = File::findOne($id);
    if (!$model) {
        throw new \yii\web\NotFoundHttpException('Файл не найден');
    }

    // Проверяем права доступа
    if (!\Yii::$app->user->can('downloadFile', ['model' => $model])) {
        throw new \yii\web\ForbiddenHttpException('Доступ запрещён');
    }

    $filePath = \Yii::getAlias('@webroot/') . $model->path;
    if (!is_file($filePath)) {
        throw new \yii\web\NotFoundHttpException('Файл не существует');
    }

    return \Yii::$app->response->sendFile($filePath, $model->original_name);
}

Метод sendFile отправляет файл в браузер с правильными заголовками. Он автоматически обрабатывает большие файлы и поддерживает частичную загрузку (для докачки).

Работа с изображениями и их обработка

Часто нужно не просто сохранить картинку, но и создать превью или изменить размер. Для этого в Yii2 есть расширение yii2-imagine — обёртка над библиотекой Imagine.

composer require yiisoft/yii2-imagine

Пример использования:

use yii\imagine\Image;

// Создаём превью 100x100
Image::thumbnail('uploads/image.jpg', 100, 100)
    ->save('uploads/thumbs/image.jpg', ['quality' => 80]);

Также можно изменять размер с сохранением пропорций, добавлять водяные знаки и т.д. Всё это интуитивно понятно.

Обработка ошибок и отладка

При работе с файлами часто возникают ошибки: неверные права на папку, превышение лимита памяти, неверный путь. Вот несколько советов:

  • Всегда проверяйте права на папку загрузки. Обычно нужны права 755 или 775.
  • Если файл не загружается, проверьте upload_max_filesize и post_max_size в php.ini.
  • Используйте Yii::$app->errorHandler, чтобы логировать ошибки загрузки.
  • Включите режим отладки Yii2, чтобы видеть подробные сообщения об ошибках.

Для отладки загрузки файлов можно использовать следующий код в контроллере:

if ($model->validate()) {
    // всё ок
} else {
    var_dump($model->errors);
    die;
}

Но не забывайте убирать такие вещи в продакшене.

Заключение

Работа с файлами в Yii2 — это не ракетостроение, но требует внимания к деталям. Используйте нормальное хранилище, переименовывайте файлы, проверяйте типы и не забывайте про права доступа. Тогда ваш код будет надёжным и безопасным.

Если вы хотите сэкономить время на разработке или вам нужна помощь с проектом на Yii2 — обращайтесь в студию WNDER. Мы поможем и с файлами, и с более сложными задачами.

Студия WNDER